From d5e178de3a855747f7b47135c6c4a67ab522bac4 Mon Sep 17 00:00:00 2001 From: Luis Meyer Date: Thu, 28 May 2026 09:13:11 +0200 Subject: [PATCH] Revert "Add oidc support (#374)" This reverts commit 72d36511d0e2a78da7328705b12dbbb9fac83c9c. --- .changeset/twelve-shoes-behave.md | 13 - packages/adapter-vercel/src/index.ts | 22 +- packages/prepare-flags-definitions/README.md | 2 +- .../src/index.test.ts | 277 ++------------- .../prepare-flags-definitions/src/index.ts | 336 ++++++------------ packages/vercel-flags-core/CLAUDE.md | 2 +- packages/vercel-flags-core/package.json | 1 - .../vercel-flags-core/src/controller/auth.ts | 90 ----- .../src/controller/bundled-source.ts | 19 +- .../src/controller/fetch-datafile.ts | 7 +- .../vercel-flags-core/src/controller/index.ts | 20 +- .../src/controller/normalized-options.ts | 9 +- .../src/controller/polling-source.ts | 3 +- .../src/controller/stream-connection.ts | 16 +- .../src/controller/stream-source.ts | 40 +-- .../src/create-raw-client.ts | 2 +- .../vercel-flags-core/src/index.make.test.ts | 29 +- packages/vercel-flags-core/src/index.make.ts | 40 ++- packages/vercel-flags-core/src/types.ts | 5 +- .../src/types/flags-definitions.d.ts | 2 +- .../utils/read-bundled-definitions.test.ts | 44 +-- .../src/utils/read-bundled-definitions.ts | 23 +- .../src/utils/usage-tracker.test.ts | 22 +- .../src/utils/usage-tracker.ts | 7 +- pnpm-lock.yaml | 84 ++--- 25 files changed, 316 insertions(+), 799 deletions(-) delete mode 100644 .changeset/twelve-shoes-behave.md delete mode 100644 packages/vercel-flags-core/src/controller/auth.ts diff --git a/.changeset/twelve-shoes-behave.md b/.changeset/twelve-shoes-behave.md deleted file mode 100644 index 4dbe53f8..00000000 --- a/.changeset/twelve-shoes-behave.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -"@flags-sdk/vercel": minor -"@vercel/prepare-flags-definitions": minor -"@vercel/flags-core": minor ---- - -Add OIDC authentication support for Vercel Flags clients and generated flag definitions. - -`@vercel/flags-core` can now create clients without an SDK key and authenticate with a Vercel OIDC token, while still supporting SDK keys and connection strings. Bundled definitions can be looked up by SDK key hash or OIDC project ID. - -`@vercel/prepare-flags-definitions` now collects both SDK keys and `VERCEL_OIDC_TOKEN`, fetches definitions for each auth entry, deduplicates identical definitions across SDK keys and OIDC project IDs, and writes generated maps keyed by SDK key hash or project ID. - -`@flags-sdk/vercel` now supports provider data lookup for Vercel flag origins that do not include an SDK key, allowing OIDC-backed clients to resolve project metadata. diff --git a/packages/adapter-vercel/src/index.ts b/packages/adapter-vercel/src/index.ts index 3ecb8919..db495de3 100644 --- a/packages/adapter-vercel/src/index.ts +++ b/packages/adapter-vercel/src/index.ts @@ -23,10 +23,10 @@ export type VercelAdapterDeclaration = Omit< */ export function createVercelAdapter( // usually a connection string, but can also be a pre-configured FlagsClient - sdkKeyOrFlagsClient?: string | FlagsClient, + sdkKeyOrFlagsClient: string | FlagsClient, ) { const flagsClient = - typeof sdkKeyOrFlagsClient === 'string' || sdkKeyOrFlagsClient === undefined + typeof sdkKeyOrFlagsClient === 'string' ? createClient(sdkKeyOrFlagsClient) : sdkKeyOrFlagsClient; @@ -86,13 +86,9 @@ export function vercelAdapter(): Adapter< return defaultVercelAdapter(); } -const flagsClients = new Map(); +const flagsClients = new Map(); -/** - * Ensures we only ever create a single client per SDK Key - * When undefined is passed, due to OIDC being used, then we return a single client too. - **/ -function getOrCreateClient(sdkKey?: string): FlagsClient { +function getOrCreateClient(sdkKey: string): FlagsClient { let client = flagsClients.get(sdkKey); if (!client) { client = createClient(sdkKey); @@ -103,12 +99,14 @@ function getOrCreateClient(sdkKey?: string): FlagsClient { function isVercelOrigin( origin: unknown, -): origin is { provider: 'vercel'; sdkKey?: string } { +): origin is { provider: 'vercel'; sdkKey: string } { return ( typeof origin === 'object' && origin !== null && 'provider' in origin && - (origin as Record).provider === 'vercel' + (origin as Record).provider === 'vercel' && + 'sdkKey' in origin && + typeof (origin as Record).sdkKey === 'string' ); } @@ -124,14 +122,14 @@ export async function getProviderData( .filter((i): i is KeyedFlagDefinitionType => !Array.isArray(i)); // Collect unique sdkKeys and resolve their projectIds - const sdkKeys = new Set(); + const sdkKeys = new Set(); for (const d of flagDefs) { if (isVercelOrigin(d.origin)) { sdkKeys.add(d.origin.sdkKey); } } - const projectIdBySdkKey = new Map(); + const projectIdBySdkKey = new Map(); await Promise.all( Array.from(sdkKeys).map(async (sdkKey) => { const client = getOrCreateClient(sdkKey); diff --git a/packages/prepare-flags-definitions/README.md b/packages/prepare-flags-definitions/README.md index 98c62ae8..3a84c9cc 100644 --- a/packages/prepare-flags-definitions/README.md +++ b/packages/prepare-flags-definitions/README.md @@ -22,7 +22,7 @@ const result = await prepareFlagsDefinitions({ }); if (result.created) { - console.log(`Bundled definitions for ${result.entryCount} SDK keys`); + console.log(`Bundled definitions for ${result.sdkKeysCount} SDK keys`); } else { console.log(`No definitions created: ${result.reason}`); } diff --git a/packages/prepare-flags-definitions/src/index.test.ts b/packages/prepare-flags-definitions/src/index.test.ts index d61445d8..2dc557a2 100644 --- a/packages/prepare-flags-definitions/src/index.test.ts +++ b/packages/prepare-flags-definitions/src/index.test.ts @@ -1,23 +1,11 @@ -import { readFile } from 'node:fs/promises'; import { describe, expect, it, vi } from 'vitest'; import { version as pkgVersion } from '../package.json'; import { generateDefinitionsModule, - getProjectIdFromOidcToken, hashSdkKey, prepareFlagsDefinitions, } from './index'; -function createOidcToken(projectId: string): string { - const header = Buffer.from(JSON.stringify({ alg: 'none' })).toString( - 'base64url', - ); - const payload = Buffer.from( - JSON.stringify({ project_id: projectId, exp: 4_102_444_800 }), - ).toString('base64url'); - return `${header}.${payload}.signature`; -} - describe('hashSdkKey', () => { it('returns a SHA-256 hex digest', () => { const hash = hashSdkKey('vf_server_test_key'); @@ -33,133 +21,80 @@ describe('hashSdkKey', () => { }); }); -describe('getProjectIdFromOidcToken', () => { - it('reads the project_id claim', () => { - expect(getProjectIdFromOidcToken(createOidcToken('prj_test'))).toBe( - 'prj_test', - ); - }); -}); - describe('generateDefinitionsModule', () => { it('generates a valid JS module', () => { - const result = generateDefinitionsModule( - [{ key: 'vf_server_key1', definitions: { flag_a: { value: true } } }], - undefined, - ); + const sdkKeys = ['vf_server_key1']; + const values = [{ flag_a: { value: true } }]; + const result = generateDefinitionsModule(sdkKeys, values); expect(result).toContain('const memo'); - expect(result).toContain('export function get(key)'); + expect(result).toContain('export function get(hashedSdkKey)'); expect(result).toContain('export const version'); - expect(result).toContain('vf_server_key1'); + expect(result).toContain(hashSdkKey('vf_server_key1')); }); it('deduplicates identical definitions', () => { + const sdkKeys = ['vf_server_key1', 'vf_client_key2']; const sharedDef = { flag_a: { value: true } }; - const result = generateDefinitionsModule( - [ - { key: 'vf_server_key1', definitions: sharedDef }, - { key: 'vf_client_key2', definitions: sharedDef }, - ], - undefined, - ); + const values = [sharedDef, sharedDef]; + const result = generateDefinitionsModule(sdkKeys, values); const memoMatches = result.match(/const _d\d+ = memo/g); expect(memoMatches).toHaveLength(1); }); it('keeps separate definitions when values differ', () => { - const result = generateDefinitionsModule( - [ - { key: 'vf_server_key1', definitions: { flag_a: { value: true } } }, - { key: 'vf_client_key2', definitions: { flag_b: { value: false } } }, - ], - undefined, - ); + const sdkKeys = ['vf_server_key1', 'vf_client_key2']; + const values = [{ flag_a: { value: true } }, { flag_b: { value: false } }]; + const result = generateDefinitionsModule(sdkKeys, values); const memoMatches = result.match(/const _d\d+ = memo/g); expect(memoMatches).toHaveLength(2); }); it('maps each SDK key hash to the correct definition index', () => { - const result = generateDefinitionsModule( - [ - { key: 'vf_server_key1', definitions: { flag_a: true } }, - { key: 'vf_client_key2', definitions: { flag_b: false } }, - ], - undefined, - ); + const sdkKeys = ['vf_server_key1', 'vf_client_key2']; + const values = [{ flag_a: true }, { flag_b: false }]; + const result = generateDefinitionsModule(sdkKeys, values); - expect(result).toContain(`${JSON.stringify('vf_server_key1')}: _d0`); - expect(result).toContain(`${JSON.stringify('vf_client_key2')}: _d1`); + expect(result).toContain( + `${JSON.stringify(hashSdkKey('vf_server_key1'))}: _d0`, + ); + expect(result).toContain( + `${JSON.stringify(hashSdkKey('vf_client_key2'))}: _d1`, + ); }); it('handles empty input', () => { - const result = generateDefinitionsModule([], undefined); + const result = generateDefinitionsModule([], []); expect(result).toContain('const map = {'); - expect(result).toContain('export function get(key)'); - }); - - it('deduplicates definitions across SDK keys and OIDC project IDs', () => { - const sharedDef = { flag_a: { value: true } }; - const result = generateDefinitionsModule( - [ - { key: 'vf_server_key1', definitions: sharedDef }, - { key: 'prj_test', definitions: sharedDef }, - ], - undefined, - ); - - const memoMatches = result.match(/const _d\d+ = memo/g); - expect(memoMatches).toHaveLength(1); - expect(result).toContain(`${JSON.stringify('vf_server_key1')}: _d0`); - expect(result).toContain(`${JSON.stringify('prj_test')}: _d0`); + expect(result).toContain('export function get(hashedSdkKey)'); }); }); describe('prepareFlagsDefinitions', () => { - it('returns { created: false, reason: "no-flags-entries" } when no flags auth is in env', async () => { + it('returns { created: false, reason: "no-sdk-keys" } when no SDK keys in env', async () => { const result = await prepareFlagsDefinitions({ cwd: '/tmp/test', env: { SOME_VAR: 'hello' }, }); - expect(result).toEqual({ created: false, reason: 'no-flags-entries' }); + expect(result).toEqual({ created: false, reason: 'no-sdk-keys' }); }); - it('returns { created: true, entryCount: N } when definitions are created', async () => { + it('returns { created: true, sdkKeysCount: N } when definitions are created', async () => { const mockFetch = async () => new Response(JSON.stringify({ flag_a: { value: true } }), { status: 200, }); - const cwd = '/tmp/test-definitions'; const result = await prepareFlagsDefinitions({ - cwd, + cwd: '/tmp/test-definitions', env: { FLAGS_SECRET: 'vf_server_test_key_123' }, fetch: mockFetch, }); - expect(result).toEqual({ created: true, entryCount: 1 }); - const definitionsJs = await readFile( - `${cwd}/node_modules/@vercel/flags-definitions/index.js`, - 'utf8', - ); - expect(definitionsJs).toMatchInlineSnapshot(` - "const memo = (fn) => { let cached; return () => (cached ??= fn()); }; - - const _d0 = memo(() => JSON.parse("{\\"flag_a\\":{\\"value\\":true}}")); - - const map = { - "faab116281fa4201059a73f3ca8b7cad7fce9e1132988008784883fa2c78d64a": _d0, - }; - - export function get(key) { - return map[key]?.() ?? null; - } - - export const version = "1.0.1";" - `); + expect(result).toEqual({ created: true, sdkKeysCount: 1 }); }); it('sends default user-agent with package version', async () => { @@ -212,7 +147,7 @@ describe('prepareFlagsDefinitions', () => { fetch: mockFetch, }); - expect(result).toEqual({ created: false, reason: 'no-flags-entries' }); + expect(result).toEqual({ created: false, reason: 'no-sdk-keys' }); expect(mockFetch).not.toHaveBeenCalled(); }); @@ -222,38 +157,18 @@ describe('prepareFlagsDefinitions', () => { json: () => Promise.resolve({ flag_a: { value: true } }), }); - const cwd = '/tmp/test-flags-format'; const result = await prepareFlagsDefinitions({ - cwd, + cwd: '/tmp/test-flags-format', env: { FLAGS_CONNECTION: 'flags:sdkKey=vf_server_my_key&other=value', }, fetch: mockFetch, }); - expect(result).toEqual({ created: true, entryCount: 1 }); + expect(result).toEqual({ created: true, sdkKeysCount: 1 }); expect(mockFetch).toHaveBeenCalledTimes(1); const headers = mockFetch.mock.calls[0]?.[1]?.headers; expect(headers.authorization).toBe('Bearer vf_server_my_key'); - const definitionsJs = await readFile( - `${cwd}/node_modules/@vercel/flags-definitions/index.js`, - 'utf8', - ); - expect(definitionsJs).toMatchInlineSnapshot(` - "const memo = (fn) => { let cached; return () => (cached ??= fn()); }; - - const _d0 = memo(() => JSON.parse("{\\"flag_a\\":{\\"value\\":true}}")); - - const map = { - "3790790d2dc9b23c4539a9f3c49eb5820e4216daebdd7eeee9136f3ceccc31a3": _d0, - }; - - export function get(key) { - return map[key]?.() ?? null; - } - - export const version = "1.0.1";" - `); }); it('ignores invalid SDK keys in flags: connection string', async () => { @@ -267,137 +182,7 @@ describe('prepareFlagsDefinitions', () => { fetch: mockFetch, }); - expect(result).toEqual({ created: false, reason: 'no-flags-entries' }); + expect(result).toEqual({ created: false, reason: 'no-sdk-keys' }); expect(mockFetch).not.toHaveBeenCalled(); }); - - it('stores OIDC definitions under the token project_id', async () => { - const mockFetch = vi.fn().mockResolvedValue({ - ok: true, - json: () => Promise.resolve({ flag_a: { value: true } }), - }); - - const cwd = '/tmp/test-oidc-definitions'; - const result = await prepareFlagsDefinitions({ - cwd, - env: { VERCEL_OIDC_TOKEN: createOidcToken('prj_oidc_test') }, - fetch: mockFetch, - }); - - expect(result).toEqual({ created: true, entryCount: 1 }); - expect(mockFetch).toHaveBeenCalledTimes(1); - const headers = mockFetch.mock.calls[0]?.[1]?.headers; - expect(headers.authorization).toBe( - `Bearer ${createOidcToken('prj_oidc_test')}`, - ); - - const definitionsJs = await readFile( - `${cwd}/node_modules/@vercel/flags-definitions/index.js`, - 'utf8', - ); - expect(definitionsJs).toMatchInlineSnapshot(` - "const memo = (fn) => { let cached; return () => (cached ??= fn()); }; - - const _d0 = memo(() => JSON.parse("{\\"flag_a\\":{\\"value\\":true}}")); - - const map = { - "prj_oidc_test": _d0, - }; - - export function get(key) { - return map[key]?.() ?? null; - } - - export const version = "1.0.1";" - `); - }); - - it('stores OIDC definitions alongside SDK Keys', async () => { - const mockFetch = vi.fn().mockResolvedValue({ - ok: true, - json: () => Promise.resolve({ flag_a: { value: true } }), - }); - - const cwd = '/tmp/test-oidc-sdk-key-mix'; - const result = await prepareFlagsDefinitions({ - cwd, - env: { - VERCEL_OIDC_TOKEN: createOidcToken('prj_oidc_test'), - FLAGS: 'vf_server_test_key_123', - }, - fetch: mockFetch, - }); - - expect(result).toEqual({ created: true, entryCount: 2 }); - expect(mockFetch).toHaveBeenCalledTimes(2); - - const definitionsJs = await readFile( - `${cwd}/node_modules/@vercel/flags-definitions/index.js`, - 'utf8', - ); - expect(definitionsJs).toMatchInlineSnapshot(` - "const memo = (fn) => { let cached; return () => (cached ??= fn()); }; - - const _d0 = memo(() => JSON.parse("{\\"flag_a\\":{\\"value\\":true}}")); - - const map = { - "faab116281fa4201059a73f3ca8b7cad7fce9e1132988008784883fa2c78d64a": _d0, - "prj_oidc_test": _d0, - }; - - export function get(key) { - return map[key]?.() ?? null; - } - - export const version = "1.0.1";" - `); - }); - - it('stores OIDC definitions alongside SDK Keys with different datafiles', async () => { - const mockFetch = vi - .fn() - .mockResolvedValueOnce({ - ok: true, - json: () => Promise.resolve({ flag_a: { value: true } }), - }) - .mockResolvedValueOnce({ - ok: true, - json: () => Promise.resolve({ flag_b: { value: true } }), - }); - - const cwd = '/tmp/test-oidc-sdk-key-mix'; - const result = await prepareFlagsDefinitions({ - cwd, - env: { - VERCEL_OIDC_TOKEN: createOidcToken('prj_oidc_test'), - FLAGS: 'vf_server_test_key_123', - }, - fetch: mockFetch, - }); - - expect(result).toEqual({ created: true, entryCount: 2 }); - expect(mockFetch).toHaveBeenCalledTimes(2); - - const definitionsJs = await readFile( - `${cwd}/node_modules/@vercel/flags-definitions/index.js`, - 'utf8', - ); - expect(definitionsJs).toMatchInlineSnapshot(` - "const memo = (fn) => { let cached; return () => (cached ??= fn()); }; - - const _d0 = memo(() => JSON.parse("{\\"flag_a\\":{\\"value\\":true}}")); - const _d1 = memo(() => JSON.parse("{\\"flag_b\\":{\\"value\\":true}}")); - - const map = { - "faab116281fa4201059a73f3ca8b7cad7fce9e1132988008784883fa2c78d64a": _d0, - "prj_oidc_test": _d1, - }; - - export function get(key) { - return map[key]?.() ?? null; - } - - export const version = "1.0.1";" - `); - }); }); diff --git a/packages/prepare-flags-definitions/src/index.ts b/packages/prepare-flags-definitions/src/index.ts index bc5d18e9..47798a43 100644 --- a/packages/prepare-flags-definitions/src/index.ts +++ b/packages/prepare-flags-definitions/src/index.ts @@ -14,8 +14,8 @@ export interface Output { } export type PrepareFlagsDefinitionsResult = - | { created: false; reason: 'no-flags-entries' } - | { created: true; entryCount: number }; + | { created: false; reason: 'no-sdk-keys' } + | { created: true; sdkKeysCount: number }; /** * Obfuscates SDK key for logging (shows first 18 chars) @@ -34,128 +34,13 @@ export function hashSdkKey(sdkKey: string): string { return createHash('sha256').update(sdkKey).digest('hex'); } -export function getProjectIdFromOidcToken(oidcToken: string) { - const tokenParts = oidcToken.split('.'); - if (tokenParts.length !== 3 || !tokenParts[1]) { - return; - } - - const payload = JSON.parse( - Buffer.from(tokenParts[1], 'base64url').toString('utf8'), - ) as { project_id?: unknown }; - - if (typeof payload.project_id !== 'string' || !payload.project_id) { - return; - } - - return payload.project_id; -} - -type DefinitionsEntry = { - key: string; - definitions: BundledDefinitions; -}; - -type MapEntry = { - key: string; - value: string; -}; - /** - * Creates js constants pointing to memoized deduplicated flag definitions. - * Output format: - * ```js - * const _d0 = memo(() => JSON.parse('...')); - * const _d1 = memo(() => JSON.parse('...')); - * ```` - */ -function generateDefinitionConstants( - lines: string[], - entries: DefinitionsEntry[], -): MapEntry[] { - const stringToConst = new Map(); - - return entries.map((entry) => { - const stringified = JSON.stringify(entry.definitions); - let definitionConst = stringToConst.get(stringified); - - if (!definitionConst) { - definitionConst = `_d${stringToConst.size}`; - stringToConst.set(stringified, definitionConst); - lines.push( - `const ${definitionConst} = memo(() => JSON.parse(${JSON.stringify(stringified)}));`, - ); - } - - return { key: entry.key, value: definitionConst }; - }); -} - -/** - * Creates a js map and getter function for exposing key value pairs. - * Output format: - * ```js - * const map = { "": _d0 }; - * export function get(key) { return map[key]?.() ?? null; } - * ```` + * Regex to match valid Vercel Flags SDK keys. + * SDK keys must follow the format: vf_server_* or vf_client_* + * This avoids false positives with third-party identifiers that happen + * to start with 'vf_' (e.g., Stripe identity flow IDs like 'vf_1PyH...'). */ -function generateMap(lines: string[], entries: MapEntry[]): void { - lines.push(''); - lines.push('const map = {'); - for (const entry of entries) { - lines.push(` ${JSON.stringify(entry.key)}: ${entry.value},`); - } - lines.push('};'); - lines.push(''); - lines.push('export function get(key) {'); - lines.push(' return map[key]?.() ?? null;'); - lines.push('}'); -} - -async function fetchDatafile( - token: string, - env: Record, - fetchFn: typeof globalThis.fetch, - userAgentSuffix?: string, -): Promise { - const headers: Record = { - authorization: `Bearer ${token}`, - 'user-agent': [ - `@vercel/prepare-flags-definitions/${PACKAGE_VERSION}`, - userAgentSuffix, - ] - .filter(Boolean) - .join(' '), - }; - - // Add Vercel metadata headers if available - if (env.VERCEL_PROJECT_ID) { - headers['x-vercel-project-id'] = env.VERCEL_PROJECT_ID; - } - if (env.VERCEL_ENV) { - headers['x-vercel-env'] = env.VERCEL_ENV; - } - if (env.VERCEL_DEPLOYMENT_ID) { - headers['x-vercel-deployment-id'] = env.VERCEL_DEPLOYMENT_ID; - } - if (env.VERCEL_REGION) { - headers['x-vercel-region'] = env.VERCEL_REGION; - } - - const res = await fetchFn(`${FLAGS_HOST}/v1/datafile`, { headers }); - - if (!res.ok) { - if (res.status === 404) { - return undefined; - } - - throw new Error( - `Failed to fetch flag definitions for ${obfuscate(token)}: ${res.status} ${res.statusText}`, - ); - } - - return res.json() as Promise; -} +const SDK_KEY_REGEX = /^vf_(?:server|client)_/; /** * Generates a JS module with deduplicated, lazily-parsed definitions. @@ -172,92 +57,61 @@ async function fetchDatafile( * ``` */ export function generateDefinitionsModule( - entries: DefinitionsEntry[], - output: Output | undefined, + sdkKeys: string[], + values: BundledDefinitions[], ): string { - output?.debug( - `vercel-flags: writing flag definitions for "${entries.map(({ key }) => obfuscate(key)).join(', ')}"`, + // Stringify each definition + const stringified = sdkKeys.map((_, i) => JSON.stringify(values[i])); + + // Deduplicate: map unique strings to indices + const uniqueStrings: string[] = []; + const stringToIndex = new Map(); + for (const s of stringified) { + if (!stringToIndex.has(s)) { + stringToIndex.set(s, uniqueStrings.length); + uniqueStrings.push(s); + } + } + + // Map SDK keys to their definition index + const keyToIndex = sdkKeys.map( + (_, i) => stringToIndex.get(stringified[i]!) ?? 0, ); - // generate shared js + // Hash each SDK key + const hashedKeys = sdkKeys.map(hashSdkKey); + + // Generate JS const lines: string[] = [ 'const memo = (fn) => { let cached; return () => (cached ??= fn()); };', '', ]; - // generate js const and capture the const names - const generatedDefinitions = generateDefinitionConstants(lines, entries); - - // generate a map wiring keys to const names - generateMap(lines, generatedDefinitions); + // Add definition constants + for (let i = 0; i < uniqueStrings.length; i++) { + lines.push( + `const _d${i} = memo(() => JSON.parse(${JSON.stringify(uniqueStrings[i])}));`, + ); + } + lines.push(''); + lines.push('const map = {'); + for (let i = 0; i < sdkKeys.length; i++) { + lines.push(` ${JSON.stringify(hashedKeys[i])}: _d${keyToIndex[i]},`); + } + lines.push('};'); + lines.push(''); + lines.push('export function get(hashedSdkKey) {'); + lines.push(' return map[hashedSdkKey]?.() ?? null;'); + lines.push('}'); + lines.push(''); lines.push( - '', `export const version = ${JSON.stringify(FLAGS_DEFINITIONS_VERSION)};`, ); return lines.join('\n'); } -type FlagEntry = { - type: 'oidcToken' | 'sdkKey'; - key: string; -}; - -/** - * Regex to match valid Vercel Flags SDK keys. - * SDK keys must follow the format: vf_server_* or vf_client_* - * This avoids false positives with third-party identifiers that happen - * to start with 'vf_' (e.g., Stripe identity flow IDs like 'vf_1PyH...'). - */ -const SDK_KEY_REGEX = /^vf_(?:server|client)_/; - -/** - * Collect all possible flag entries the need embedding from the environment - */ -function collectFlagEntries( - env: Record, - output: Output | undefined, -): FlagEntry[] { - const entries: FlagEntry[] = []; - - // Collect unique SDK keys from environment variables - // Supports both direct SDK keys (vf_server_*/vf_client_*) and flags: format - const sdkKeys = Array.from( - Object.values(env).reduce>((acc, value) => { - if (typeof value === 'string') { - if (SDK_KEY_REGEX.test(value)) { - acc.add(value); - } else if (value.startsWith('flags:')) { - const params = new URLSearchParams(value.slice('flags:'.length)); - const sdkKey = params.get('sdkKey'); - if (sdkKey && SDK_KEY_REGEX.test(sdkKey)) { - acc.add(sdkKey); - } - } - } - return acc; - }, new Set()), - ); - - if (sdkKeys.length > 0) { - output?.debug(`vercel-flags: found ${sdkKeys.length} SDK keys`); - - for (const key of sdkKeys) { - entries.push({ type: 'sdkKey', key }); - } - } - - const oidcToken = env.VERCEL_OIDC_TOKEN; - if (oidcToken && oidcToken?.length > 0) { - output?.debug(`vercel-flags: found OIDC token`); - - entries.push({ type: 'oidcToken', key: oidcToken }); - } - - return entries; -} - /** * Prepares flag definitions by reading SDK keys from environment variables, * fetching definitions from flags.vercel.com, and writing them into a @@ -282,49 +136,78 @@ export async function prepareFlagsDefinitions(options: { output, } = options; - output?.debug('vercel-flags: checking env vars for SDK Keys and OIDC Token'); + output?.debug('vercel-flags: checking env vars for SDK Keys'); + + // Collect unique SDK keys from environment variables + // Supports both direct SDK keys (vf_server_*/vf_client_*) and flags: format + const sdkKeys = Array.from( + Object.values(env).reduce>((acc, value) => { + if (typeof value === 'string') { + if (SDK_KEY_REGEX.test(value)) { + acc.add(value); + } else if (value.startsWith('flags:')) { + const params = new URLSearchParams(value.slice('flags:'.length)); + const sdkKey = params.get('sdkKey'); + if (sdkKey && SDK_KEY_REGEX.test(sdkKey)) { + acc.add(sdkKey); + } + } + } + return acc; + }, new Set()), + ); + + output?.debug(`vercel-flags: found ${sdkKeys.length} SDK keys`); - const entries = collectFlagEntries(env, output); - if (entries.length === 0) { - return { created: false, reason: 'no-flags-entries' }; + if (sdkKeys.length === 0) { + return { created: false, reason: 'no-sdk-keys' }; } - // fetch all datafiles for sdk keys and oidc tokens - const resolvedEntries = await Promise.all( - entries.map(async ({ key, type }) => { - const definitions = await fetchDatafile( - key, - env, - fetchFn, - userAgentSuffix, - ); - if (!definitions) { - return; + // Fetch definitions for each SDK key + const fetchPromise = Promise.all( + sdkKeys.map(async (sdkKey) => { + const headers: Record = { + authorization: `Bearer ${sdkKey}`, + 'user-agent': [ + `@vercel/prepare-flags-definitions/${PACKAGE_VERSION}`, + userAgentSuffix, + ] + .filter(Boolean) + .join(' '), + }; + + // Add Vercel metadata headers if available + if (env.VERCEL_PROJECT_ID) { + headers['x-vercel-project-id'] = env.VERCEL_PROJECT_ID; + } + if (env.VERCEL_ENV) { + headers['x-vercel-env'] = env.VERCEL_ENV; + } + if (env.VERCEL_DEPLOYMENT_ID) { + headers['x-vercel-deployment-id'] = env.VERCEL_DEPLOYMENT_ID; + } + if (env.VERCEL_REGION) { + headers['x-vercel-region'] = env.VERCEL_REGION; } - if (type === 'oidcToken') { - const projectId = getProjectIdFromOidcToken(key); + const res = await fetchFn(`${FLAGS_HOST}/v1/datafile`, { headers }); - if (projectId) { - return { - key: projectId, - definitions, - }; - } + if (!res.ok) { + throw new Error( + `Failed to fetch flag definitions for ${obfuscate(sdkKey)}: ${res.status} ${res.statusText}`, + ); } - if (type === 'sdkKey') { - return { - key: hashSdkKey(key), - definitions, - }; - } + return res.json() as Promise; }), ); - const validEntries = resolvedEntries.filter((entry) => !!entry); + const values = output + ? await output.time('vercel-flags: load datafiles', fetchPromise) + : await fetchPromise; - const definitionsJs = generateDefinitionsModule(validEntries, output); + // Generate the JS module + const definitionsJs = generateDefinitionsModule(sdkKeys, values); // Write to node_modules/@vercel/flags-definitions/ const storageDir = join(cwd, 'node_modules', '@vercel', 'flags-definitions'); @@ -333,7 +216,7 @@ export async function prepareFlagsDefinitions(options: { const packageJsonPath = join(storageDir, 'package.json'); const dts = [ - 'export function get(key: string): Record | null;', + 'export function get(hashedSdkKey: string): Record | null;', 'export const version: string;', '', ].join('\n'); @@ -363,6 +246,9 @@ export async function prepareFlagsDefinitions(options: { output?.debug(` → ${indexPath}`); output?.debug(` → ${dtsPath}`); output?.debug(` → ${packageJsonPath}`); + output?.debug( + ` → included definitions for keys "${sdkKeys.map((key) => obfuscate(key)).join(', ')}"`, + ); - return { created: true, entryCount: entries.length }; + return { created: true, sdkKeysCount: sdkKeys.length }; } diff --git a/packages/vercel-flags-core/CLAUDE.md b/packages/vercel-flags-core/CLAUDE.md index a2409216..69fb0314 100644 --- a/packages/vercel-flags-core/CLAUDE.md +++ b/packages/vercel-flags-core/CLAUDE.md @@ -84,7 +84,7 @@ type FlagsClient = { ```typescript type ControllerOptions = { - sdkKey?: string; + sdkKey: string; datafile?: Datafile; // Initial datafile for immediate reads stream?: boolean | { initTimeoutMs: number }; // default: true (3000ms) polling?: boolean | { intervalMs: number; initTimeoutMs: number }; // default: true (30s interval, 3s timeout) diff --git a/packages/vercel-flags-core/package.json b/packages/vercel-flags-core/package.json index 8a708654..cc1a4c3f 100644 --- a/packages/vercel-flags-core/package.json +++ b/packages/vercel-flags-core/package.json @@ -74,7 +74,6 @@ }, "dependencies": { "@vercel/functions": "^3.4.3", - "@vercel/oidc": "3.4.0", "jose": "5.2.1", "js-xxhash": "4.0.0" }, diff --git a/packages/vercel-flags-core/src/controller/auth.ts b/packages/vercel-flags-core/src/controller/auth.ts deleted file mode 100644 index d2f9b986..00000000 --- a/packages/vercel-flags-core/src/controller/auth.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { getVercelOidcToken } from '@vercel/oidc'; -import { parseSdkKeyFromFlagsConnectionString } from '../utils/sdk-keys'; - -export type BundledDefinitionsLookup = - | { type: 'sdk-key'; sdkKey: string } - | { type: 'project-id'; projectId: string }; - -export interface Auth { - sdkKey?: string; - resolveToken(): Promise; - resolveBundledDefinitionsLookup(): Promise; -} - -async function getOidcToken(): Promise { - try { - return await getVercelOidcToken(); - } catch { - throw new Error( - [ - '@vercel/flags-core: Failed to get OIDC token.', - 'Are you running in a Vercel Environment where OIDC tokens are available?', - 'Did you mean to use an SDK Key instead? Use the environment variable FLAGS or pass it directly to the client.', - ].join(' '), - ); - } -} - -function getProjectIdFromOidcToken(oidcToken: string): string { - const tokenParts = oidcToken.split('.'); - if (tokenParts.length !== 3 || !tokenParts[1]) { - throw new Error('@vercel/flags-core: Invalid OIDC token'); - } - - const payload = JSON.parse( - Buffer.from(tokenParts[1], 'base64url').toString('utf8'), - ) as { project_id?: unknown }; - - if (typeof payload.project_id !== 'string' || !payload.project_id) { - throw new Error( - '@vercel/flags-core: Missing project_id claim in OIDC token', - ); - } - - return payload.project_id; -} - -export class Authentication implements Auth { - public readonly sdkKey?: string; - - constructor(sdkKeyOrConnectionString: string | undefined) { - // validate sdk key format - if (sdkKeyOrConnectionString !== undefined) { - if (typeof sdkKeyOrConnectionString !== 'string') { - throw new Error( - `@vercel/flags-core: Invalid sdkKey. Expected string, got ${typeof sdkKeyOrConnectionString}`, - ); - } - - // Parse connection string if needed (e.g., "flags:edgeConfigId=...&sdkKey=vf_xxx") - const parsed = parseSdkKeyFromFlagsConnectionString( - sdkKeyOrConnectionString, - ); - if (!parsed) { - throw new Error('@vercel/flags-core: Missing sdkKey'); - } - - this.sdkKey = parsed; - } - } - - public async resolveToken() { - if (this.sdkKey) { - return this.sdkKey; - } - - return await getOidcToken(); - } - - public async resolveBundledDefinitionsLookup(): Promise { - if (this.sdkKey) { - return { type: 'sdk-key', sdkKey: this.sdkKey }; - } - - const oidcToken = await this.resolveToken(); - return { - type: 'project-id', - projectId: getProjectIdFromOidcToken(oidcToken), - }; - } -} diff --git a/packages/vercel-flags-core/src/controller/bundled-source.ts b/packages/vercel-flags-core/src/controller/bundled-source.ts index 139dbc79..8a7a47de 100644 --- a/packages/vercel-flags-core/src/controller/bundled-source.ts +++ b/packages/vercel-flags-core/src/controller/bundled-source.ts @@ -5,7 +5,6 @@ import type { DatafileInput, } from '../types'; import type { readBundledDefinitions } from '../utils/read-bundled-definitions'; -import type { Auth } from './auth'; /** * Manages loading of bundled flag definitions. @@ -13,13 +12,17 @@ import type { Auth } from './auth'; */ export class BundledSource { private promise: Promise | undefined; + private options: { + sdkKey: string; + readBundledDefinitions: typeof readBundledDefinitions; + }; - constructor( - private options: { - auth: Auth; - readBundledDefinitions: typeof readBundledDefinitions; - }, - ) {} + constructor(options: { + sdkKey: string; + readBundledDefinitions: typeof readBundledDefinitions; + }) { + this.options = options; + } /** * Load bundled definitions. @@ -73,7 +76,7 @@ export class BundledSource { private getResult(): Promise { if (!this.promise) { - this.promise = this.options.readBundledDefinitions(this.options.auth); + this.promise = this.options.readBundledDefinitions(this.options.sdkKey); } return this.promise; } diff --git a/packages/vercel-flags-core/src/controller/fetch-datafile.ts b/packages/vercel-flags-core/src/controller/fetch-datafile.ts index 6427e7dd..b63b01b6 100644 --- a/packages/vercel-flags-core/src/controller/fetch-datafile.ts +++ b/packages/vercel-flags-core/src/controller/fetch-datafile.ts @@ -1,6 +1,5 @@ import { version } from '../../package.json'; import type { BundledDefinitions } from '../types'; -import type { Auth } from './auth'; const DEFAULT_FETCH_TIMEOUT_MS = 10_000; @@ -9,12 +8,10 @@ const DEFAULT_FETCH_TIMEOUT_MS = 10_000; */ export async function fetchDatafile(options: { host: string; - auth: Auth; + sdkKey: string; fetch: typeof globalThis.fetch; signal?: AbortSignal; }): Promise { - const token = await options.auth.resolveToken(); - const controller = new AbortController(); const timeoutId = setTimeout( () => controller.abort(), @@ -34,7 +31,7 @@ export async function fetchDatafile(options: { try { const res = await options.fetch(`${options.host}/v1/datafile`, { headers: { - Authorization: `Bearer ${token}`, + Authorization: `Bearer ${options.sdkKey}`, 'User-Agent': `VercelFlagsCore/${version}`, ...(process.env.VERCEL_ENV ? { 'X-Vercel-Env': process.env.VERCEL_ENV } diff --git a/packages/vercel-flags-core/src/controller/index.ts b/packages/vercel-flags-core/src/controller/index.ts index 5fd6db29..c173e69b 100644 --- a/packages/vercel-flags-core/src/controller/index.ts +++ b/packages/vercel-flags-core/src/controller/index.ts @@ -119,6 +119,16 @@ export class Controller implements ControllerInterface { private unauthorized = false; constructor(options: ControllerOptions) { + if ( + !options.sdkKey || + typeof options.sdkKey !== 'string' || + !options.sdkKey.startsWith('vf_') + ) { + throw new Error( + '@vercel/flags-core: SDK key must be a string starting with "vf_"', + ); + } + this.options = normalizeOptions(options); // Create source modules @@ -130,7 +140,7 @@ export class Controller implements ControllerInterface { this.pollingSource = new PollingSource(this.options); this.bundledSource = new BundledSource({ - auth: this.options.auth, + sdkKey: this.options.sdkKey, readBundledDefinitions, }); @@ -378,7 +388,7 @@ export class Controller implements ControllerInterface { try { const fetched = await fetchDatafile({ host: this.options.host, - auth: this.options.auth, + sdkKey: this.options.sdkKey, fetch: this.options.fetch, }); this.data = tagData(fetched, 'fetched'); @@ -614,7 +624,7 @@ export class Controller implements ControllerInterface { try { const fetched = await fetchDatafile({ host: this.options.host, - auth: this.options.auth, + sdkKey: this.options.sdkKey, fetch: this.options.fetch, }); return tagData(fetched, 'fetched'); @@ -655,7 +665,7 @@ export class Controller implements ControllerInterface { try { const fetched = await fetchDatafile({ host: this.options.host, - auth: this.options.auth, + sdkKey: this.options.sdkKey, fetch: this.options.fetch, }); this.data = tagData(fetched, 'fetched'); @@ -720,7 +730,7 @@ export class Controller implements ControllerInterface { try { const fetched = await fetchDatafile({ host: this.options.host, - auth: this.options.auth, + sdkKey: this.options.sdkKey, fetch: this.options.fetch, }); this.data = tagData(fetched, 'fetched'); diff --git a/packages/vercel-flags-core/src/controller/normalized-options.ts b/packages/vercel-flags-core/src/controller/normalized-options.ts index f17b15a4..5474e4a3 100644 --- a/packages/vercel-flags-core/src/controller/normalized-options.ts +++ b/packages/vercel-flags-core/src/controller/normalized-options.ts @@ -1,5 +1,4 @@ import type { DatafileInput, PollingOptions, StreamOptions } from '../types'; -import type { Auth } from './auth'; const DEFAULT_STREAM_INIT_TIMEOUT_MS = 3000; const DEFAULT_POLLING_INTERVAL_MS = 30_000; @@ -10,8 +9,8 @@ const DEFAULT_POLLING_INIT_TIMEOUT_MS = 3_000; * Configuration options for Controller */ export type ControllerOptions = { - /** Authentication which resolves the token for requests */ - auth: Auth; + /** SDK key for authentication (must start with "vf_") */ + sdkKey: string; /** * Initial datafile to use immediately @@ -55,7 +54,7 @@ export type ControllerOptions = { }; export type NormalizedOptions = { - auth: Auth; + sdkKey: string; datafile: DatafileInput | undefined; stream: { enabled: boolean; initTimeoutMs: number }; polling: { enabled: boolean; intervalMs: number; initTimeoutMs: number }; @@ -104,7 +103,7 @@ export function normalizeOptions( } return { - auth: options.auth, + sdkKey: options.sdkKey, datafile: options.datafile, stream, polling, diff --git a/packages/vercel-flags-core/src/controller/polling-source.ts b/packages/vercel-flags-core/src/controller/polling-source.ts index e39cd191..59c808cb 100644 --- a/packages/vercel-flags-core/src/controller/polling-source.ts +++ b/packages/vercel-flags-core/src/controller/polling-source.ts @@ -1,11 +1,10 @@ import type { DatafileInput } from '../types'; -import type { Auth } from './auth'; import { fetchDatafile } from './fetch-datafile'; import { TypedEmitter } from './typed-emitter'; export type PollingSourceConfig = { host: string; - auth: Auth; + sdkKey: string; polling: { intervalMs: number; }; diff --git a/packages/vercel-flags-core/src/controller/stream-connection.ts b/packages/vercel-flags-core/src/controller/stream-connection.ts index 4019ee21..fcf4c156 100644 --- a/packages/vercel-flags-core/src/controller/stream-connection.ts +++ b/packages/vercel-flags-core/src/controller/stream-connection.ts @@ -43,8 +43,7 @@ export type StreamCallbacks = { export type StreamConfig = { host: string; - token?: string; - sdkKey?: string; + sdkKey: string; abortController: AbortController; fetch?: typeof globalThis.fetch; /** Returns the current revision number to send as X-Revision header */ @@ -60,11 +59,12 @@ export async function connectStream( config: StreamConfig, callbacks: StreamCallbacks, ): Promise { - const { host, abortController, fetch: fetchFn = globalThis.fetch } = config; - const token = config.token ?? config.sdkKey; - if (!token) { - throw new Error('stream: missing auth token'); - } + const { + host, + sdkKey, + abortController, + fetch: fetchFn = globalThis.fetch, + } = config; const { onDatafile, onPrimed, onDisconnect } = callbacks; let retryCount = 0; let lastAttemptTime = 0; @@ -115,7 +115,7 @@ export async function connectStream( try { lastAttemptTime = Date.now(); const headers: Record = { - Authorization: `Bearer ${token}`, + Authorization: `Bearer ${sdkKey}`, 'User-Agent': `VercelFlagsCore/${version}`, 'X-Retry-Attempt': String(retryCount), }; diff --git a/packages/vercel-flags-core/src/controller/stream-source.ts b/packages/vercel-flags-core/src/controller/stream-source.ts index 0940d752..93fba85c 100644 --- a/packages/vercel-flags-core/src/controller/stream-source.ts +++ b/packages/vercel-flags-core/src/controller/stream-source.ts @@ -52,29 +52,27 @@ export class StreamSource extends TypedEmitter { ); try { - const promise = this.options.auth.resolveToken().then((token) => - connectStream( - { - host: this.options.host, - token, - abortController, - fetch: this.options.fetch, - revision: this.revision, + const promise = connectStream( + { + host: this.options.host, + sdkKey: this.options.sdkKey, + abortController, + fetch: this.options.fetch, + revision: this.revision, + }, + { + onDatafile: (newData) => { + this.emit('data', newData); + this.emit('connected'); }, - { - onDatafile: (newData) => { - this.emit('data', newData); - this.emit('connected'); - }, - onPrimed: (message) => { - this.emit('primed', message); - this.emit('connected'); - }, - onDisconnect: () => { - this.emit('disconnected'); - }, + onPrimed: (message) => { + this.emit('primed', message); + this.emit('connected'); }, - ), + onDisconnect: () => { + this.emit('disconnected'); + }, + }, ); this.promise = promise; diff --git a/packages/vercel-flags-core/src/create-raw-client.ts b/packages/vercel-flags-core/src/create-raw-client.ts index e9a773b8..a9d2494d 100644 --- a/packages/vercel-flags-core/src/create-raw-client.ts +++ b/packages/vercel-flags-core/src/create-raw-client.ts @@ -45,7 +45,7 @@ export function createCreateRawClient(fns: { origin, }: { controller: ControllerInterface; - origin?: { provider: string; sdkKey?: string }; + origin?: { provider: string; sdkKey: string }; }): FlagsClient { const id = idCount++; controllerInstanceMap.set(id, { diff --git a/packages/vercel-flags-core/src/index.make.test.ts b/packages/vercel-flags-core/src/index.make.test.ts index 89e70e9c..3651aa54 100644 --- a/packages/vercel-flags-core/src/index.make.test.ts +++ b/packages/vercel-flags-core/src/index.make.test.ts @@ -4,8 +4,8 @@ import { make } from './index.make'; // Mock the Controller to avoid real network calls vi.mock('./controller', () => ({ - Controller: vi.fn().mockImplementation(({ auth }) => ({ - auth, + Controller: vi.fn().mockImplementation(({ sdkKey }) => ({ + sdkKey, read: vi.fn().mockResolvedValue({ projectId: 'test', definitions: {}, @@ -20,7 +20,7 @@ vi.mock('./controller', () => ({ import { Controller } from './controller'; function createMockCreateRawClient(): ReturnType { - return vi.fn().mockImplementation(({ controller }) => ({ + return vi.fn().mockImplementation(({ dataSource }) => ({ initialize: vi.fn().mockResolvedValue(undefined), shutdown: vi.fn().mockResolvedValue(undefined), getDatafile: vi.fn().mockResolvedValue({ @@ -38,7 +38,7 @@ function createMockCreateRawClient(): ReturnType { revision: 1, }), evaluate: vi.fn().mockResolvedValue({ value: true, reason: 'static' }), - _dataSource: controller, // For testing inspection + _dataSource: dataSource, // For testing inspection })); } @@ -63,7 +63,7 @@ describe('make', () => { const client = createClient('vf_server_test_key'); expect(Controller).toHaveBeenCalledWith({ - auth: expect.objectContaining({ sdkKey: 'vf_server_test_key' }), + sdkKey: 'vf_server_test_key', }); expect(createRawClient).toHaveBeenCalled(); expect(client).toBeDefined(); @@ -78,7 +78,7 @@ describe('make', () => { const client = createClient(connectionString); expect(Controller).toHaveBeenCalledWith({ - auth: expect.objectContaining({ sdkKey: 'vf_client_conn_key' }), + sdkKey: 'vf_client_conn_key', }); expect(client).toBeDefined(); }); @@ -127,16 +127,15 @@ describe('make', () => { expect(createRawClient).toHaveBeenCalledTimes(1); }); - it('should create an OIDC-authenticated default client if FLAGS env var is missing', () => { + it('should throw if FLAGS env var is missing when accessed', () => { const createRawClient = createMockCreateRawClient(); delete process.env.FLAGS; const { flagsClient } = make(createRawClient); - const _ = flagsClient.evaluate; - expect(Controller).toHaveBeenCalledWith({ - auth: expect.objectContaining({ sdkKey: undefined }), - }); + expect(() => flagsClient.evaluate).toThrow( + 'flags: Missing environment variable FLAGS', + ); }); it('should throw if FLAGS env var has invalid value', () => { @@ -173,7 +172,7 @@ describe('make', () => { const _ = flagsClient.evaluate; expect(Controller).toHaveBeenCalledWith({ - auth: expect.objectContaining({ sdkKey: 'vf_server_env_key' }), + sdkKey: 'vf_server_env_key', }); }); @@ -186,7 +185,7 @@ describe('make', () => { const _ = flagsClient.evaluate; expect(Controller).toHaveBeenCalledWith({ - auth: expect.objectContaining({ sdkKey: 'vf_client_flags_key' }), + sdkKey: 'vf_client_flags_key', }); }); }); @@ -219,7 +218,7 @@ describe('make', () => { // Access with first key const _ = flagsClient.evaluate; expect(Controller).toHaveBeenCalledWith({ - auth: expect.objectContaining({ sdkKey: 'vf_server_first_key' }), + sdkKey: 'vf_server_first_key', }); // Reset and change env @@ -229,7 +228,7 @@ describe('make', () => { // Access again with new key const __ = flagsClient.initialize; expect(Controller).toHaveBeenCalledWith({ - auth: expect.objectContaining({ sdkKey: 'vf_client_second_key' }), + sdkKey: 'vf_client_second_key', }); }); }); diff --git a/packages/vercel-flags-core/src/index.make.ts b/packages/vercel-flags-core/src/index.make.ts index 1c5aaa5a..bdb0aef2 100644 --- a/packages/vercel-flags-core/src/index.make.ts +++ b/packages/vercel-flags-core/src/index.make.ts @@ -3,14 +3,14 @@ */ import { Controller, type ControllerOptions } from './controller'; -import { Authentication } from './controller/auth'; import type { createCreateRawClient } from './create-raw-client'; import type { FlagsClient } from './types'; +import { parseSdkKeyFromFlagsConnectionString } from './utils/sdk-keys'; /** * Options for createClient */ -export type CreateClientOptions = Omit; +export type CreateClientOptions = Omit; export function make( createRawClient: ReturnType, @@ -18,7 +18,7 @@ export function make( flagsClient: FlagsClient; resetDefaultFlagsClient: () => void; createClient: >( - sdkKeyOrConnectionString?: string, + sdkKeyOrConnectionString: string, options?: CreateClientOptions, ) => FlagsClient; } { @@ -28,16 +28,32 @@ export function make( // - data source must specify the environment & projectId as sdkKey has that info // - "reuse" functionality relies on the data source having the data for all envs function createClient>( - sdkKeyOrConnectionString?: string, + sdkKeyOrConnectionString: string, options?: CreateClientOptions, ): FlagsClient { - const auth = new Authentication(sdkKeyOrConnectionString); + if (!sdkKeyOrConnectionString) + throw new Error('@vercel/flags-core: Missing sdkKey'); + + if (typeof sdkKeyOrConnectionString !== 'string') + throw new Error( + `@vercel/flags-core: Invalid sdkKey. Expected string, got ${typeof sdkKeyOrConnectionString}`, + ); + + // Parse connection string if needed (e.g., "flags:edgeConfigId=...&sdkKey=vf_xxx") + const sdkKey = parseSdkKeyFromFlagsConnectionString( + sdkKeyOrConnectionString, + ); + if (!sdkKey) { + throw new Error( + '@vercel/flags-core: Missing sdkKey in connection string', + ); + } // sdk key contains the environment - const controller = new Controller({ auth, ...options }); + const controller = new Controller({ sdkKey, ...options }); return createRawClient({ controller, - origin: { provider: 'vercel', sdkKey: auth.sdkKey }, + origin: { provider: 'vercel', sdkKey }, }); } @@ -48,7 +64,15 @@ export function make( const flagsClient: FlagsClient = new Proxy({} as FlagsClient, { get(_, prop) { if (!_defaultFlagsClient) { - _defaultFlagsClient = createClient(process.env.FLAGS); + if (!process.env.FLAGS) { + throw new Error('flags: Missing environment variable FLAGS'); + } + + const sdkKey = parseSdkKeyFromFlagsConnectionString(process.env.FLAGS); + if (!sdkKey) { + throw new Error('@vercel/flags-core: Missing sdkKey'); + } + _defaultFlagsClient = createClient(sdkKey); } return _defaultFlagsClient[prop as keyof FlagsClient]; }, diff --git a/packages/vercel-flags-core/src/types.ts b/packages/vercel-flags-core/src/types.ts index 1fe8010f..fb684939 100644 --- a/packages/vercel-flags-core/src/types.ts +++ b/packages/vercel-flags-core/src/types.ts @@ -120,12 +120,11 @@ export type Source = { */ export type FlagsClient> = { /** - * Origin information for this client. - * sdkKey is only present when the client was explicitly created with one. + * Origin information for this client (provider and sdkKey) */ origin?: { provider: string; - sdkKey?: string; + sdkKey: string; }; /** * Evaluate a feature flag diff --git a/packages/vercel-flags-core/src/types/flags-definitions.d.ts b/packages/vercel-flags-core/src/types/flags-definitions.d.ts index 88e1b022..fb289556 100644 --- a/packages/vercel-flags-core/src/types/flags-definitions.d.ts +++ b/packages/vercel-flags-core/src/types/flags-definitions.d.ts @@ -1,4 +1,4 @@ declare module '@vercel/flags-definitions' { - export function get(key: string): Record | null; + export function get(hashedSdkKey: string): Record | null; export const version: string; } diff --git a/packages/vercel-flags-core/src/utils/read-bundled-definitions.test.ts b/packages/vercel-flags-core/src/utils/read-bundled-definitions.test.ts index 6ec08fa6..336b28e1 100644 --- a/packages/vercel-flags-core/src/utils/read-bundled-definitions.test.ts +++ b/packages/vercel-flags-core/src/utils/read-bundled-definitions.test.ts @@ -1,5 +1,4 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import type { Auth } from '../controller/auth'; // The readBundledDefinitions function uses dynamic import which is hard to mock. // Instead, we test the behavior indirectly through the Controller @@ -8,12 +7,6 @@ import type { Auth } from '../controller/auth'; describe('readBundledDefinitions', () => { const originalEnv = process.env; - const auth: Auth = { - sdkKey: 'test-id', - resolveToken: () => Promise.resolve('test-id'), - resolveBundledDefinitionsLookup: () => - Promise.resolve({ type: 'sdk-key', sdkKey: 'test-id' }), - }; beforeEach(() => { vi.resetModules(); @@ -33,7 +26,7 @@ describe('readBundledDefinitions', () => { expect(typeof readBundledDefinitions).toBe('function'); // Calling it should return a promise (will likely fail since the module doesn't exist) - const result = readBundledDefinitions(auth); + const result = readBundledDefinitions('test-id'); expect(result).toBeInstanceOf(Promise); // The result should have the expected shape @@ -47,7 +40,7 @@ describe('readBundledDefinitions', () => { './read-bundled-definitions' ); - const result = await readBundledDefinitions(auth); + const result = await readBundledDefinitions('nonexistent-id'); // Since @vercel/flags-definitions/definitions.json doesn't exist in test env, // it should return either missing-file or unexpected-error @@ -62,7 +55,7 @@ describe('readBundledDefinitions', () => { './read-bundled-definitions' ); - const result = await readBundledDefinitions(auth); + const result = await readBundledDefinitions('nonexistent-id'); expect(result).toEqual({ definitions: null, @@ -70,37 +63,6 @@ describe('readBundledDefinitions', () => { }); }); - it('should read OIDC bundled definitions by project id', async () => { - const definitions = { - projectId: 'prj_test', - environment: 'production', - definitions: {}, - configUpdatedAt: 1, - digest: 'digest', - revision: 1, - }; - const get = vi.fn((key: string) => - key === 'prj_test' ? definitions : null, - ); - vi.doMock('@vercel/flags-definitions', () => ({ get })); - - const oidcAuth: Auth = { - resolveToken: () => Promise.resolve('token'), - resolveBundledDefinitionsLookup: () => - Promise.resolve({ type: 'project-id', projectId: 'prj_test' }), - }; - - const { readBundledDefinitions } = await import( - './read-bundled-definitions' - ); - - await expect(readBundledDefinitions(oidcAuth)).resolves.toEqual({ - definitions, - state: 'ok', - }); - expect(get).toHaveBeenCalledWith('prj_test'); - }); - // The detailed behavior of readBundledDefinitions is tested indirectly // through Controller tests which mock readBundledDefinitions. // Those tests cover: diff --git a/packages/vercel-flags-core/src/utils/read-bundled-definitions.ts b/packages/vercel-flags-core/src/utils/read-bundled-definitions.ts index 87034733..aace6ce9 100644 --- a/packages/vercel-flags-core/src/utils/read-bundled-definitions.ts +++ b/packages/vercel-flags-core/src/utils/read-bundled-definitions.ts @@ -4,7 +4,6 @@ // is degraded or unavailable. // -import type { Auth, BundledDefinitionsLookup } from '../controller/auth'; import type { BundledDefinitions, BundledDefinitionsResult } from '../types'; /** In-memory cache of SDK key to its hashed value, so we don't re-hash repeatedly. */ @@ -36,7 +35,7 @@ function hashSdkKey(sdkKey: string): Promise { * Reads the local definitions that get bundled at build time. */ export async function readBundledDefinitions( - auth: Auth, + sdkKey: string, ): Promise { let get: (sdkKey: string) => BundledDefinitions | null; try { @@ -63,27 +62,13 @@ export async function readBundledDefinitions( return { definitions: null, state: 'missing-file' }; } - let lookup: BundledDefinitionsLookup; - try { - lookup = await auth.resolveBundledDefinitionsLookup(); - } catch (error) { - return { definitions: null, state: 'unexpected-error', error }; - } - - if (lookup.type === 'project-id') { - const entry = get(lookup.projectId); - return entry - ? { definitions: entry, state: 'ok' } - : { definitions: null, state: 'missing-entry' }; - } - - // Try plain sdk key first for bundles created by older CLI versions. - const entry = get(lookup.sdkKey); + // try plain sdk key first + const entry = get(sdkKey); if (entry) return { definitions: entry, state: 'ok' }; // try hashed key but catch any errors try { - const hashedKey = await hashSdkKey(lookup.sdkKey); + const hashedKey = await hashSdkKey(sdkKey); // try original key (older cli versions) and hashed key (newer cli versions) const hashedEntry = get(hashedKey); if (hashedEntry) return { definitions: hashedEntry, state: 'ok' }; diff --git a/packages/vercel-flags-core/src/utils/usage-tracker.test.ts b/packages/vercel-flags-core/src/utils/usage-tracker.test.ts index 86895f60..c2e5ace0 100644 --- a/packages/vercel-flags-core/src/utils/usage-tracker.test.ts +++ b/packages/vercel-flags-core/src/utils/usage-tracker.test.ts @@ -1,5 +1,4 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import type { Auth } from '../controller/auth'; import { setRequestContext } from '../test-utils'; import { type FlagsConfigReadEvent, UsageTracker } from './usage-tracker'; @@ -40,18 +39,9 @@ afterEach(() => { vi.unstubAllEnvs(); }); -function createAuth(sdkKey = 'test-key'): Auth { - return { - sdkKey, - resolveToken: () => Promise.resolve(sdkKey), - resolveBundledDefinitionsLookup: () => - Promise.resolve({ type: 'sdk-key', sdkKey }), - }; -} - function createTracker(sdkKey = 'test-key') { return new UsageTracker({ - auth: createAuth(sdkKey), + sdkKey, host: 'https://example.com', fetch: fetchMock, }); @@ -147,7 +137,7 @@ describe('UsageTracker', () => { fetchMock.mockImplementation(() => jsonResponse({ ok: true })); const tracker = new UsageTracker({ - auth: createAuth('my-secret-key'), + sdkKey: 'my-secret-key', host: 'https://example.com', fetch: fetchMock, }); @@ -239,7 +229,7 @@ describe('UsageTracker', () => { ); const tracker = new FreshUsageTracker({ - auth: createAuth('test-key'), + sdkKey: 'test-key', host: 'https://example.com', fetch: fetchMock, }); @@ -277,7 +267,7 @@ describe('UsageTracker', () => { fetchMock.mockImplementation(() => jsonResponse({ ok: true })); const tracker = new FreshUsageTracker({ - auth: createAuth('test-key'), + sdkKey: 'test-key', host: 'https://example.com', fetch: fetchMock, }); @@ -377,13 +367,13 @@ describe('UsageTracker', () => { }); const tracker1 = new UsageTracker({ - auth: createAuth('key-1'), + sdkKey: 'key-1', host: 'https://example.com', fetch: fetchMock, }); const tracker2 = new UsageTracker({ - auth: createAuth('key-2'), + sdkKey: 'key-2', host: 'https://example.com', fetch: fetchMock, }); diff --git a/packages/vercel-flags-core/src/utils/usage-tracker.ts b/packages/vercel-flags-core/src/utils/usage-tracker.ts index e03f53bf..033b06b2 100644 --- a/packages/vercel-flags-core/src/utils/usage-tracker.ts +++ b/packages/vercel-flags-core/src/utils/usage-tracker.ts @@ -1,6 +1,5 @@ import { waitUntil } from '@vercel/functions'; import { version } from '../../package.json'; -import type { Auth } from '../controller/auth'; import { getJitteredWaitMs, getRetryDelayMs } from './backoff'; const RESOLVED_VOID: Promise = Promise.resolve(); @@ -82,7 +81,7 @@ function getRequestContext(): RequestContext { } export interface UsageTrackerOptions { - auth: Auth; + sdkKey: string; host: string; fetch: typeof fetch; } @@ -260,8 +259,6 @@ export class UsageTracker { const flushId = ++this.flushCounter; - const token = await this.options.auth.resolveToken(); - for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { try { const response = await this.options.fetch( @@ -270,7 +267,7 @@ export class UsageTracker { method: 'POST', headers: { 'Content-Type': 'application/json', - Authorization: `Bearer ${token}`, + Authorization: `Bearer ${this.options.sdkKey}`, 'User-Agent': `VercelFlagsCore/${version}`, ...(process.env.VERCEL_ENV ? { 'X-Vercel-Env': process.env.VERCEL_ENV } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ce55f4a8..e9caede8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -40,7 +40,7 @@ importers: version: 15.2.2 next: specifier: ^16.1.6 - version: 16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.58.1)(react-dom@19.2.4(react@19.3.0-canary-f4e0d4ed-20260429))(react@19.3.0-canary-f4e0d4ed-20260429) + version: 16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.58.1)(react-dom@19.2.4(react@19.3.0-canary-561ed529-20260423))(react@19.3.0-canary-561ed529-20260423) playwright: specifier: 1.56.1 version: 1.56.1 @@ -49,7 +49,7 @@ importers: version: 0.3.14 react: specifier: canary - version: 19.3.0-canary-f4e0d4ed-20260429 + version: 19.3.0-canary-561ed529-20260423 turbo: specifier: 2.8.15 version: 2.8.15 @@ -484,7 +484,7 @@ importers: dependencies: '@vercel/edge-config': specifier: ^1.4.3 - version: 1.4.3(@opentelemetry/api@1.9.0)(next@16.2.0(@opentelemetry/api@1.9.0)(@playwright/test@1.58.1)(react-dom@19.2.4(react@19.3.0-canary-f4e0d4ed-20260429))(react@19.3.0-canary-f4e0d4ed-20260429)) + version: 1.4.3(@opentelemetry/api@1.9.0)(next@16.2.0(@opentelemetry/api@1.9.0)(@playwright/test@1.58.1)(react-dom@19.2.4(react@19.3.0-canary-561ed529-20260423))(react@19.3.0-canary-561ed529-20260423)) devDependencies: '@types/node': specifier: 20.11.17 @@ -546,7 +546,7 @@ importers: version: 1.6.1 '@vercel/edge-config': specifier: ^1.4.3 - version: 1.4.3(@opentelemetry/api@1.9.0)(next@16.2.0(@opentelemetry/api@1.9.0)(@playwright/test@1.58.1)(react-dom@19.2.4(react@19.3.0-canary-f4e0d4ed-20260429))(react@19.3.0-canary-f4e0d4ed-20260429)) + version: 1.4.3(@opentelemetry/api@1.9.0)(next@16.2.0(@opentelemetry/api@1.9.0)(@playwright/test@1.58.1)(react-dom@19.2.4(react@19.3.0-canary-561ed529-20260423))(react@19.3.0-canary-561ed529-20260423)) '@vercel/functions': specifier: ^1.5.2 version: 1.6.0 @@ -580,7 +580,7 @@ importers: dependencies: '@vercel/edge-config': specifier: ^1.4.3 - version: 1.4.3(@opentelemetry/api@1.9.0)(next@16.2.0(@opentelemetry/api@1.9.0)(@playwright/test@1.58.1)(react-dom@19.2.4(react@19.3.0-canary-f4e0d4ed-20260429))(react@19.3.0-canary-f4e0d4ed-20260429)) + version: 1.4.3(@opentelemetry/api@1.9.0)(next@16.2.0(@opentelemetry/api@1.9.0)(@playwright/test@1.58.1)(react-dom@19.2.4(react@19.3.0-canary-561ed529-20260423))(react@19.3.0-canary-561ed529-20260423)) hypertune: specifier: 2.8.3 version: 2.8.3 @@ -614,10 +614,10 @@ importers: dependencies: '@launchdarkly/vercel-server-sdk': specifier: ^1.3.34 - version: 1.3.34(@opentelemetry/api@1.9.0)(next@16.2.0(@opentelemetry/api@1.9.0)(@playwright/test@1.58.1)(react-dom@19.2.4(react@19.3.0-canary-f4e0d4ed-20260429))(react@19.3.0-canary-f4e0d4ed-20260429)) + version: 1.3.34(@opentelemetry/api@1.9.0)(next@16.2.0(@opentelemetry/api@1.9.0)(@playwright/test@1.58.1)(react-dom@19.2.4(react@19.3.0-canary-561ed529-20260423))(react@19.3.0-canary-561ed529-20260423)) '@vercel/edge-config': specifier: ^1.4.3 - version: 1.4.3(@opentelemetry/api@1.9.0)(next@16.2.0(@opentelemetry/api@1.9.0)(@playwright/test@1.58.1)(react-dom@19.2.4(react@19.3.0-canary-f4e0d4ed-20260429))(react@19.3.0-canary-f4e0d4ed-20260429)) + version: 1.4.3(@opentelemetry/api@1.9.0)(next@16.2.0(@opentelemetry/api@1.9.0)(@playwright/test@1.58.1)(react-dom@19.2.4(react@19.3.0-canary-561ed529-20260423))(react@19.3.0-canary-561ed529-20260423)) devDependencies: '@types/node': specifier: 20.11.17 @@ -705,7 +705,7 @@ importers: dependencies: '@vercel/edge-config': specifier: ^1.4.3 - version: 1.4.3(@opentelemetry/api@1.9.0)(next@16.2.0(@opentelemetry/api@1.9.0)(@playwright/test@1.58.1)(react-dom@19.2.4(react@19.3.0-canary-f4e0d4ed-20260429))(react@19.3.0-canary-f4e0d4ed-20260429)) + version: 1.4.3(@opentelemetry/api@1.9.0)(next@16.2.0(@opentelemetry/api@1.9.0)(@playwright/test@1.58.1)(react-dom@19.2.4(react@19.3.0-canary-561ed529-20260423))(react@19.3.0-canary-561ed529-20260423)) posthog-node: specifier: 4.11.1 version: 4.11.1 @@ -797,7 +797,7 @@ importers: dependencies: '@vercel/edge-config': specifier: ^1.4.3 - version: 1.4.3(@opentelemetry/api@1.9.0)(next@16.2.0(@opentelemetry/api@1.9.0)(@playwright/test@1.58.1)(react-dom@19.2.4(react@19.3.0-canary-f4e0d4ed-20260429))(react@19.3.0-canary-f4e0d4ed-20260429)) + version: 1.4.3(@opentelemetry/api@1.9.0)(next@16.2.0(@opentelemetry/api@1.9.0)(@playwright/test@1.58.1)(react-dom@19.2.4(react@19.3.0-canary-561ed529-20260423))(react@19.3.0-canary-561ed529-20260423)) '@vercel/functions': specifier: ^1.5.2 version: 1.6.0 @@ -806,7 +806,7 @@ importers: version: 0.5.2 statsig-node-vercel: specifier: ^0.7.0 - version: 0.7.0(@vercel/edge-config@1.4.3(@opentelemetry/api@1.9.0)(next@16.2.0(@opentelemetry/api@1.9.0)(@playwright/test@1.58.1)(react-dom@19.2.4(react@19.3.0-canary-f4e0d4ed-20260429))(react@19.3.0-canary-f4e0d4ed-20260429))) + version: 0.7.0(@vercel/edge-config@1.4.3(@opentelemetry/api@1.9.0)(next@16.2.0(@opentelemetry/api@1.9.0)(@playwright/test@1.58.1)(react-dom@19.2.4(react@19.3.0-canary-561ed529-20260423))(react@19.3.0-canary-561ed529-20260423))) devDependencies: '@types/node': specifier: 20.11.17 @@ -850,7 +850,7 @@ importers: version: 2.6.4(@types/node@20.11.17)(typescript@5.6.3) next: specifier: 16.1.5 - version: 16.1.5(@opentelemetry/api@1.9.0)(@playwright/test@1.58.1)(react-dom@19.2.4(react@19.3.0-canary-f4e0d4ed-20260429))(react@19.3.0-canary-f4e0d4ed-20260429) + version: 16.1.5(@opentelemetry/api@1.9.0)(@playwright/test@1.58.1)(react-dom@19.2.4(react@19.3.0-canary-561ed529-20260423))(react@19.3.0-canary-561ed529-20260423) tsup: specifier: 8.5.1 version: 8.5.1(jiti@2.6.1)(postcss@8.5.8)(typescript@5.6.3)(yaml@2.8.1) @@ -945,9 +945,6 @@ importers: '@vercel/functions': specifier: ^3.4.3 version: 3.4.3 - '@vercel/oidc': - specifier: 3.4.0 - version: 3.4.0 jose: specifier: 5.2.1 version: 5.2.1 @@ -963,7 +960,7 @@ importers: version: 20.11.17 next: specifier: ^16.1.6 - version: 16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.58.1)(react-dom@19.2.4(react@19.3.0-canary-f4e0d4ed-20260429))(react@19.3.0-canary-f4e0d4ed-20260429) + version: 16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.58.1)(react-dom@19.2.4(react@19.3.0-canary-561ed529-20260423))(react@19.3.0-canary-561ed529-20260423) tsup: specifier: 8.5.1 version: 8.5.1(jiti@2.6.1)(postcss@8.5.8)(typescript@5.6.3)(yaml@2.8.1) @@ -4969,10 +4966,6 @@ packages: resolution: {integrity: sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug==} engines: {node: '>= 20'} - '@vercel/oidc@3.4.0': - resolution: {integrity: sha512-p0sKfHkfRmMaqqDwNL4tjnX9TgRrLMlEtUjIxfrEns8pOxz1R9ztqOVI+ehqiq93/2/HnfPe/UBZkfAZwnx0UA==} - engines: {node: '>= 20'} - '@vercel/speed-insights@1.3.1': resolution: {integrity: sha512-PbEr7FrMkUrGYvlcLHGkXdCkxnylCWePx7lPxxq36DNdfo9mcUjLOmqOyPDHAOgnfqgGGdmE3XI9L/4+5fr+vQ==} peerDependencies: @@ -8283,8 +8276,8 @@ packages: resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} engines: {node: '>=0.10.0'} - react@19.3.0-canary-f4e0d4ed-20260429: - resolution: {integrity: sha512-FNfU7Fsr/U/6t76mMAOucXovXS7536HmVK4nVWKxLAOY+P7dpZ/rJfR2If4uHjAwQoMsLxZ41gG2VOWJDkprOA==} + react@19.3.0-canary-561ed529-20260423: + resolution: {integrity: sha512-tN5JiqCwYgG5kSzVIcM5Sx3NPT6+rfOCVKolHycLBZivIhfcPVQqAn5/UgSxy8QeVNrUlyniVIztNpAeKlUz5w==} engines: {node: '>=0.10.0'} read-cache@1.0.0: @@ -9178,7 +9171,6 @@ packages: uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} - deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true validate-npm-package-name@5.0.1: @@ -10638,10 +10630,10 @@ snapshots: '@launchdarkly/js-sdk-common': 2.19.0 semver: 7.5.4 - '@launchdarkly/vercel-server-sdk@1.3.34(@opentelemetry/api@1.9.0)(next@16.2.0(@opentelemetry/api@1.9.0)(@playwright/test@1.58.1)(react-dom@19.2.4(react@19.3.0-canary-f4e0d4ed-20260429))(react@19.3.0-canary-f4e0d4ed-20260429))': + '@launchdarkly/vercel-server-sdk@1.3.34(@opentelemetry/api@1.9.0)(next@16.2.0(@opentelemetry/api@1.9.0)(@playwright/test@1.58.1)(react-dom@19.2.4(react@19.3.0-canary-561ed529-20260423))(react@19.3.0-canary-561ed529-20260423))': dependencies: '@launchdarkly/js-server-sdk-common-edge': 2.6.9 - '@vercel/edge-config': 1.4.3(@opentelemetry/api@1.9.0)(next@16.2.0(@opentelemetry/api@1.9.0)(@playwright/test@1.58.1)(react-dom@19.2.4(react@19.3.0-canary-f4e0d4ed-20260429))(react@19.3.0-canary-f4e0d4ed-20260429)) + '@vercel/edge-config': 1.4.3(@opentelemetry/api@1.9.0)(next@16.2.0(@opentelemetry/api@1.9.0)(@playwright/test@1.58.1)(react-dom@19.2.4(react@19.3.0-canary-561ed529-20260423))(react@19.3.0-canary-561ed529-20260423)) crypto-js: 4.2.0 transitivePeerDependencies: - '@opentelemetry/api' @@ -13304,12 +13296,12 @@ snapshots: '@opentelemetry/api': 1.9.0 next: 16.1.5(@opentelemetry/api@1.9.0)(@playwright/test@1.58.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@vercel/edge-config@1.4.3(@opentelemetry/api@1.9.0)(next@16.2.0(@opentelemetry/api@1.9.0)(@playwright/test@1.58.1)(react-dom@19.2.4(react@19.3.0-canary-f4e0d4ed-20260429))(react@19.3.0-canary-f4e0d4ed-20260429))': + '@vercel/edge-config@1.4.3(@opentelemetry/api@1.9.0)(next@16.2.0(@opentelemetry/api@1.9.0)(@playwright/test@1.58.1)(react-dom@19.2.4(react@19.3.0-canary-561ed529-20260423))(react@19.3.0-canary-561ed529-20260423))': dependencies: '@vercel/edge-config-fs': 0.1.0 optionalDependencies: '@opentelemetry/api': 1.9.0 - next: 16.2.0(@opentelemetry/api@1.9.0)(@playwright/test@1.58.1)(react-dom@19.2.4(react@19.3.0-canary-f4e0d4ed-20260429))(react@19.3.0-canary-f4e0d4ed-20260429) + next: 16.2.0(@opentelemetry/api@1.9.0)(@playwright/test@1.58.1)(react-dom@19.2.4(react@19.3.0-canary-561ed529-20260423))(react@19.3.0-canary-561ed529-20260423) '@vercel/edge@1.2.1': {} @@ -13426,8 +13418,6 @@ snapshots: '@vercel/oidc@3.2.0': {} - '@vercel/oidc@3.4.0': {} - '@vercel/speed-insights@1.3.1(@sveltejs/kit@2.50.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.41.3)(vite@5.4.21(@types/node@24.10.13)(lightningcss@1.30.2)))(svelte@5.41.3)(typescript@5.8.2)(vite@5.4.21(@types/node@24.10.13)(lightningcss@1.30.2)))(next@16.2.0(@opentelemetry/api@1.9.0)(@playwright/test@1.58.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(svelte@5.41.3)': optionalDependencies: '@sveltejs/kit': 2.50.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.41.3)(vite@5.4.21(@types/node@24.10.13)(lightningcss@1.30.2)))(svelte@5.41.3)(typescript@5.8.2)(vite@5.4.21(@types/node@24.10.13)(lightningcss@1.30.2)) @@ -16926,16 +16916,16 @@ snapshots: - '@babel/core' - babel-plugin-macros - next@16.1.5(@opentelemetry/api@1.9.0)(@playwright/test@1.58.1)(react-dom@19.2.4(react@19.3.0-canary-f4e0d4ed-20260429))(react@19.3.0-canary-f4e0d4ed-20260429): + next@16.1.5(@opentelemetry/api@1.9.0)(@playwright/test@1.58.1)(react-dom@19.2.4(react@19.3.0-canary-561ed529-20260423))(react@19.3.0-canary-561ed529-20260423): dependencies: '@next/env': 16.1.5 '@swc/helpers': 0.5.15 baseline-browser-mapping: 2.10.0 caniuse-lite: 1.0.30001777 postcss: 8.4.31 - react: 19.3.0-canary-f4e0d4ed-20260429 - react-dom: 19.2.4(react@19.3.0-canary-f4e0d4ed-20260429) - styled-jsx: 5.1.6(react@19.3.0-canary-f4e0d4ed-20260429) + react: 19.3.0-canary-561ed529-20260423 + react-dom: 19.2.4(react@19.3.0-canary-561ed529-20260423) + styled-jsx: 5.1.6(react@19.3.0-canary-561ed529-20260423) optionalDependencies: '@next/swc-darwin-arm64': 16.1.5 '@next/swc-darwin-x64': 16.1.5 @@ -16978,16 +16968,16 @@ snapshots: - '@babel/core' - babel-plugin-macros - next@16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.58.1)(react-dom@19.2.4(react@19.3.0-canary-f4e0d4ed-20260429))(react@19.3.0-canary-f4e0d4ed-20260429): + next@16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.58.1)(react-dom@19.2.4(react@19.3.0-canary-561ed529-20260423))(react@19.3.0-canary-561ed529-20260423): dependencies: '@next/env': 16.1.6 '@swc/helpers': 0.5.15 baseline-browser-mapping: 2.10.0 caniuse-lite: 1.0.30001777 postcss: 8.4.31 - react: 19.3.0-canary-f4e0d4ed-20260429 - react-dom: 19.2.4(react@19.3.0-canary-f4e0d4ed-20260429) - styled-jsx: 5.1.6(react@19.3.0-canary-f4e0d4ed-20260429) + react: 19.3.0-canary-561ed529-20260423 + react-dom: 19.2.4(react@19.3.0-canary-561ed529-20260423) + styled-jsx: 5.1.6(react@19.3.0-canary-561ed529-20260423) optionalDependencies: '@next/swc-darwin-arm64': 16.1.6 '@next/swc-darwin-x64': 16.1.6 @@ -17030,16 +17020,16 @@ snapshots: - '@babel/core' - babel-plugin-macros - next@16.2.0(@opentelemetry/api@1.9.0)(@playwright/test@1.58.1)(react-dom@19.2.4(react@19.3.0-canary-f4e0d4ed-20260429))(react@19.3.0-canary-f4e0d4ed-20260429): + next@16.2.0(@opentelemetry/api@1.9.0)(@playwright/test@1.58.1)(react-dom@19.2.4(react@19.3.0-canary-561ed529-20260423))(react@19.3.0-canary-561ed529-20260423): dependencies: '@next/env': 16.2.0 '@swc/helpers': 0.5.15 baseline-browser-mapping: 2.10.0 caniuse-lite: 1.0.30001777 postcss: 8.4.31 - react: 19.3.0-canary-f4e0d4ed-20260429 - react-dom: 19.2.4(react@19.3.0-canary-f4e0d4ed-20260429) - styled-jsx: 5.1.6(react@19.3.0-canary-f4e0d4ed-20260429) + react: 19.3.0-canary-561ed529-20260423 + react-dom: 19.2.4(react@19.3.0-canary-561ed529-20260423) + styled-jsx: 5.1.6(react@19.3.0-canary-561ed529-20260423) optionalDependencies: '@next/swc-darwin-arm64': 16.2.0 '@next/swc-darwin-x64': 16.2.0 @@ -17538,9 +17528,9 @@ snapshots: react: 19.2.4 scheduler: 0.27.0 - react-dom@19.2.4(react@19.3.0-canary-f4e0d4ed-20260429): + react-dom@19.2.4(react@19.3.0-canary-561ed529-20260423): dependencies: - react: 19.3.0-canary-f4e0d4ed-20260429 + react: 19.3.0-canary-561ed529-20260423 scheduler: 0.27.0 react-is@16.13.1: {} @@ -17648,7 +17638,7 @@ snapshots: react@19.2.4: {} - react@19.3.0-canary-f4e0d4ed-20260429: {} + react@19.3.0-canary-561ed529-20260423: {} read-cache@1.0.0: dependencies: @@ -18159,9 +18149,9 @@ snapshots: transitivePeerDependencies: - encoding - statsig-node-vercel@0.7.0(@vercel/edge-config@1.4.3(@opentelemetry/api@1.9.0)(next@16.2.0(@opentelemetry/api@1.9.0)(@playwright/test@1.58.1)(react-dom@19.2.4(react@19.3.0-canary-f4e0d4ed-20260429))(react@19.3.0-canary-f4e0d4ed-20260429))): + statsig-node-vercel@0.7.0(@vercel/edge-config@1.4.3(@opentelemetry/api@1.9.0)(next@16.2.0(@opentelemetry/api@1.9.0)(@playwright/test@1.58.1)(react-dom@19.2.4(react@19.3.0-canary-561ed529-20260423))(react@19.3.0-canary-561ed529-20260423))): dependencies: - '@vercel/edge-config': 1.4.3(@opentelemetry/api@1.9.0)(next@16.2.0(@opentelemetry/api@1.9.0)(@playwright/test@1.58.1)(react-dom@19.2.4(react@19.3.0-canary-f4e0d4ed-20260429))(react@19.3.0-canary-f4e0d4ed-20260429)) + '@vercel/edge-config': 1.4.3(@opentelemetry/api@1.9.0)(next@16.2.0(@opentelemetry/api@1.9.0)(@playwright/test@1.58.1)(react-dom@19.2.4(react@19.3.0-canary-561ed529-20260423))(react@19.3.0-canary-561ed529-20260423)) statsig-node-lite: 0.4.4 transitivePeerDependencies: - encoding @@ -18325,10 +18315,10 @@ snapshots: client-only: 0.0.1 react: 19.2.0 - styled-jsx@5.1.6(react@19.3.0-canary-f4e0d4ed-20260429): + styled-jsx@5.1.6(react@19.3.0-canary-561ed529-20260423): dependencies: client-only: 0.0.1 - react: 19.3.0-canary-f4e0d4ed-20260429 + react: 19.3.0-canary-561ed529-20260423 stylis@4.3.6: {}