diff --git a/packages/core/package.json b/packages/core/package.json index 2658db3c0..5e53d53fe 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -69,6 +69,7 @@ "react-native": ">=0.63.4 <1.0" }, "devDependencies": { + "@openfeature/core": "^1.9.2", "@testing-library/react-native": "7.0.2", "react-native-builder-bob": "0.26.0" }, @@ -116,6 +117,7 @@ } }, "dependencies": { + "@datadog/flagging-core": "^1.2.1", "big-integer": "^1.6.52" } } diff --git a/packages/core/src/flags/configuration/__tests__/precomputed.test.ts b/packages/core/src/flags/configuration/__tests__/precomputed.test.ts new file mode 100644 index 000000000..28b2e6928 --- /dev/null +++ b/packages/core/src/flags/configuration/__tests__/precomputed.test.ts @@ -0,0 +1,276 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +import { InternalLog } from '../../../InternalLog'; +import { + UnsupportedConfigurationError, + decodePrecomputedFlags +} from '../precomputed'; +import type { + PrecomputedConfigurationResponse, + PrecomputedFlag +} from '../types'; + +jest.mock('../../../InternalLog', () => { + return { + InternalLog: { log: jest.fn() }, + DATADOG_MESSAGE_PREFIX: 'DATADOG:' + }; +}); + +const flag = (overrides: Partial): PrecomputedFlag => ({ + variationType: 'boolean', + variationValue: true, + variationKey: 'true', + allocationKey: 'alloc-1', + reason: 'STATIC', + doLog: false, + extraLogging: {}, + ...overrides +}); + +const responseWith = ( + flags: Record, + obfuscated = false +): PrecomputedConfigurationResponse => ({ + data: { + id: '2', + type: 'precomputed-assignments', + attributes: { + obfuscated, + createdAt: '2026-07-06T23:01:56.822171460Z', + format: 'PRECOMPUTED', + environment: { name: 'Staging' }, + flags + } + } +}); + +describe('decodePrecomputedFlags', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('maps each variation type to a FlagCacheEntry with the correct value + string form', () => { + const cache = decodePrecomputedFlags( + responseWith({ + bool: flag({ + variationType: 'boolean', + variationValue: false, + variationKey: 'false' + }), + str: flag({ + variationType: 'string', + variationValue: 'hello', + variationKey: 'Hello' + }), + num: flag({ + variationType: 'number', + variationValue: 42, + variationKey: '42' + }), + int: flag({ + variationType: 'integer', + variationValue: 7, + variationKey: '7' + }), + flt: flag({ + variationType: 'float', + variationValue: 1.5, + variationKey: '1.5' + }), + obj: flag({ + variationType: 'object', + variationValue: { greeting: 'hi' }, + variationKey: 'Greeting' + }) + }) + ); + + expect(cache.bool).toEqual({ + key: 'bool', + value: false, + allocationKey: 'alloc-1', + variationKey: 'false', + variationType: 'boolean', + variationValue: 'false', + reason: 'STATIC', + doLog: false, + extraLogging: {} + }); + expect(cache.str.value).toBe('hello'); + expect(cache.str.variationValue).toBe('hello'); + expect(cache.num.value).toBe(42); + expect(cache.num.variationValue).toBe('42'); + // integer/float keep their wire variationType but decode to a JS number. + expect(cache.int.value).toBe(7); + expect(cache.int.variationType).toBe('integer'); + expect(cache.int.variationValue).toBe('7'); + expect(cache.flt.value).toBe(1.5); + expect(cache.flt.variationType).toBe('float'); + expect(cache.flt.variationValue).toBe('1.5'); + // objects are JSON-encoded for the string form; value stays an object. + expect(cache.obj.value).toEqual({ greeting: 'hi' }); + expect(cache.obj.variationValue).toBe('{"greeting":"hi"}'); + }); + + it('uses the flag map key as the entry key', () => { + const cache = decodePrecomputedFlags( + responseWith({ 'my-feature': flag({}) }) + ); + + expect(cache['my-feature'].key).toBe('my-feature'); + }); + + it('defaults missing extraLogging to an empty object', () => { + const cache = decodePrecomputedFlags( + responseWith({ f: flag({ extraLogging: undefined }) }) + ); + + expect(cache.f.extraLogging).toEqual({}); + }); + + it('tolerates a null serialId', () => { + const cache = decodePrecomputedFlags( + responseWith({ f: flag({ serialId: null }) }) + ); + + expect(cache.f.key).toBe('f'); + }); + + it('omits flags with an unsupported variation type and logs a warning', () => { + const cache = decodePrecomputedFlags( + responseWith({ + good: flag({}), + bad: flag({ variationType: 'timestamp' }) + }) + ); + + expect(cache.good).toBeDefined(); + expect(cache.bad).toBeUndefined(); + expect(InternalLog.log).toHaveBeenCalled(); + }); + + it('omits flags whose value does not match their variation type', () => { + const cache = decodePrecomputedFlags( + responseWith({ + mismatched: flag({ + variationType: 'number', + variationValue: 'not-a-number' + }) + }) + ); + + expect(cache.mismatched).toBeUndefined(); + expect(InternalLog.log).toHaveBeenCalled(); + }); + + it('omits a non-object flag entry and keeps the valid ones', () => { + const cache = decodePrecomputedFlags( + responseWith({ + good: flag({}), + bad: (null as unknown) as PrecomputedFlag + }) + ); + + expect(cache.good).toBeDefined(); + expect(cache.bad).toBeUndefined(); + expect(InternalLog.log).toHaveBeenCalled(); + }); + + it('omits a flag with malformed metadata field types', () => { + const cache = decodePrecomputedFlags( + responseWith({ + badReason: flag({ reason: (42 as unknown) as string }), + badDoLog: flag({ doLog: ('yes' as unknown) as boolean }) + }) + ); + + expect(cache.badReason).toBeUndefined(); + expect(cache.badDoLog).toBeUndefined(); + expect(InternalLog.log).toHaveBeenCalled(); + }); + + it('throws UnsupportedConfigurationError for an obfuscated response', () => { + expect(() => + decodePrecomputedFlags(responseWith({ f: flag({}) }, true)) + ).toThrow(UnsupportedConfigurationError); + }); + + it('returns an empty map when there are no flags', () => { + expect(decodePrecomputedFlags(responseWith({}))).toEqual({}); + }); + + it('omits an integer flag with a fractional value', () => { + const cache = decodePrecomputedFlags( + responseWith({ + frac: flag({ variationType: 'integer', variationValue: 7.9 }) + }) + ); + + expect(cache.frac).toBeUndefined(); + expect(InternalLog.log).toHaveBeenCalled(); + }); + + it('omits a number flag whose value is not finite', () => { + const cache = decodePrecomputedFlags( + responseWith({ + inf: flag({ variationType: 'number', variationValue: Infinity }) + }) + ); + + expect(cache.inf).toBeUndefined(); + }); + + it('returns an empty map for a structurally broken response', () => { + expect( + decodePrecomputedFlags( + ({} as unknown) as Parameters[0] + ) + ).toEqual({}); + }); + + it('accepts any JSON value for an object flag (array, null, primitive)', () => { + // ffe-service enforces a top-level object at the API layer, but that is not a + // storage constraint, so the decoder accepts whatever JSON arrives here. + const cache = decodePrecomputedFlags( + responseWith({ + arr: flag({ + variationType: 'object', + variationValue: [1, 2, 3] + }), + nul: flag({ + variationType: 'object', + variationValue: null + }), + str: flag({ + variationType: 'object', + variationValue: 'hi' + }) + }) + ); + + expect(cache.arr.value).toEqual([1, 2, 3]); + expect(cache.arr.variationValue).toBe('[1,2,3]'); + expect(cache.nul.value).toBeNull(); + expect(cache.nul.variationValue).toBe('null'); + expect(cache.str.value).toBe('hi'); + expect(cache.str.variationValue).toBe('hi'); + }); + + it('stores a flag keyed "__proto__" as data without polluting the prototype', () => { + // Computed key mirrors how JSON.parse yields an own "__proto__" property. + const cache = decodePrecomputedFlags( + responseWith({ ['__proto__']: flag({ variationValue: true }) }) + ); + + // Stored as an own data property, not via the Object.prototype setter. + expect(Object.getPrototypeOf(cache)).toBe(Object.prototype); + expect(Object.keys(cache)).toContain('__proto__'); + // No global prototype pollution. + expect(({} as Record).variationType).toBeUndefined(); + }); +}); diff --git a/packages/core/src/flags/configuration/__tests__/wire.test.ts b/packages/core/src/flags/configuration/__tests__/wire.test.ts new file mode 100644 index 000000000..23f1d359b --- /dev/null +++ b/packages/core/src/flags/configuration/__tests__/wire.test.ts @@ -0,0 +1,127 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +import type { ParsedFlagsConfiguration } from '../types'; +import { configurationFromString, configurationToString } from '../wire'; + +const buildResponse = () => ({ + data: { + id: '2', + type: 'precomputed-assignments', + attributes: { + obfuscated: false, + createdAt: '2026-07-06T23:01:56.822171460Z', + format: 'PRECOMPUTED', + environment: { name: 'Staging' }, + flags: { + 'a-flag': { + variationType: 'boolean', + variationValue: true, + variationKey: 'true', + allocationKey: 'alloc-1', + reason: 'STATIC', + doLog: false, + extraLogging: {} + }, + 'num-flag': { + variationType: 'number', + variationValue: 1.5, + variationKey: '1.5', + allocationKey: 'alloc-2', + reason: 'STATIC', + doLog: true, + extraLogging: {} + }, + 'obj-flag': { + variationType: 'object', + variationValue: { nested: { a: 1 }, list: [1, 2] }, + variationKey: 'obj', + allocationKey: 'alloc-3', + reason: 'TARGETING_MATCH', + doLog: false, + extraLogging: { extra: 'x' } + } + } + } + } +}); + +const buildWire = (overrides: Record = {}) => + JSON.stringify({ + version: 1, + precomputed: { + response: JSON.stringify(buildResponse()), + context: { targetingKey: 'user-1', country: 'US' }, + fetchedAt: 1748449320785 + }, + ...overrides + }); + +describe('configurationFromString', () => { + it('parses a valid v1 wire with a precomputed branch', () => { + const config = configurationFromString(buildWire()); + + expect(config.precomputed).toBeDefined(); + expect(config.precomputed?.context).toEqual({ + targetingKey: 'user-1', + country: 'US' + }); + expect(config.precomputed?.fetchedAt).toBe(1748449320785); + // The inner `response` string is parsed into an object. + expect( + config.precomputed?.response.data.attributes.flags['a-flag'] + .variationValue + ).toBe(true); + }); + + it('returns an empty config for an unsupported version', () => { + const wire = JSON.stringify({ + version: 2, + precomputed: { response: JSON.stringify(buildResponse()) } + }); + + expect(configurationFromString(wire)).toEqual({}); + }); + + it('returns an empty config for invalid JSON', () => { + expect(configurationFromString('not json')).toEqual({}); + }); + + it('returns an empty config when the inner response is invalid JSON', () => { + const wire = JSON.stringify({ + version: 1, + precomputed: { response: '{ not json' } + }); + + expect(configurationFromString(wire)).toEqual({}); + }); + + it('returns a config with no precomputed branch when none is present', () => { + const wire = JSON.stringify({ version: 1 }); + + expect(configurationFromString(wire)).toEqual({}); + }); +}); + +describe('configurationToString round-trip', () => { + it('round-trips a precomputed configuration', () => { + const original = configurationFromString(buildWire()); + + const restored = configurationFromString( + configurationToString(original) + ); + + expect(restored).toEqual(original); + }); + + it('serializes an empty configuration to a v1 wire', () => { + const empty: ParsedFlagsConfiguration = {}; + + expect(configurationToString(empty)).toBe( + JSON.stringify({ version: 1 }) + ); + }); +}); diff --git a/packages/core/src/flags/configuration/index.ts b/packages/core/src/flags/configuration/index.ts new file mode 100644 index 000000000..3890eefc3 --- /dev/null +++ b/packages/core/src/flags/configuration/index.ts @@ -0,0 +1,23 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +// Internal module boundary for portable-configuration handling. Intentionally NOT +// re-exported from the package's public entry point (`packages/core/src/index.tsx`) +// until the exports step (FFL-2690). Keeping the surface behind this boundary makes a +// future "port -> depend on a shared core" swap contained. + +export { configurationFromString, configurationToString } from './wire'; +export { + decodePrecomputedFlags, + UnsupportedConfigurationError +} from './precomputed'; +export type { + ParsedFlagsConfiguration, + ParsedPrecomputedConfiguration, + PrecomputedConfigurationResponse, + PrecomputedFlag, + WireEvaluationContext +} from './types'; diff --git a/packages/core/src/flags/configuration/precomputed.ts b/packages/core/src/flags/configuration/precomputed.ts new file mode 100644 index 000000000..d795c7c6e --- /dev/null +++ b/packages/core/src/flags/configuration/precomputed.ts @@ -0,0 +1,194 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +import { InternalLog } from '../../InternalLog'; +import { SdkVerbosity } from '../../config/types/SdkVerbosity'; +import type { FlagCacheEntry } from '../internal'; + +import type { + PrecomputedConfigurationResponse, + PrecomputedFlag +} from './types'; +import { SUPPORTED_VARIATION_TYPES } from './types'; + +/** + * Thrown when a configuration cannot be supported by this SDK (e.g. an obfuscated + * precomputed payload). Callers translate this into a provider error state rather + * than silently serving wrong data. + */ +export class UnsupportedConfigurationError extends Error { + constructor(message: string) { + super(message); + this.name = 'UnsupportedConfigurationError'; + } +} + +/** + * Decode a precomputed CDN response into the `FlagCacheEntry` map that `FlagsClient` + * caches and evaluates against — the same shape the native CDN fetch returns today. + * + * The mapping is ~1:1. Two transforms are applied per flag: + * - `value` is the typed `variationValue` as-is (`integer`/`float` are JS `number`s); + * - a string `variationValue` is derived (JSON for objects, `"true"/"false"` for + * booleans, `String(...)` otherwise) because native Android exposure tracking + * rebuilds the flag from the string form. + * + * @throws {UnsupportedConfigurationError} if the response is obfuscated. + */ +export const decodePrecomputedFlags = ( + response: PrecomputedConfigurationResponse +): Record => { + const attributes = response?.data?.attributes; + + // `obfuscated` is not part of flagging-core's response type, but the CDN payload + // carries it. Read it defensively so obfuscated payloads are still rejected: + // de-hashing keys / decoding values is not implemented here, so fail predictably + // instead of mis-mapping hashed keys. + if ((attributes as { obfuscated?: boolean } | undefined)?.obfuscated) { + throw new UnsupportedConfigurationError( + 'Obfuscated precomputed configurations are not supported.' + ); + } + + const flags = attributes?.flags ?? {}; + // Accumulate in a Map so a pathological flag keyed "__proto__" is stored as data + // instead of hitting the `Object.prototype` "__proto__" setter (which a plain + // `obj[key] = ...` assignment would). `Object.fromEntries` then materializes own + // properties without invoking inherited setters. + const cache = new Map(); + + for (const [key, flag] of Object.entries(flags)) { + const entry = toFlagCacheEntry(key, flag); + if (entry) { + cache.set(key, entry); + } + } + + return Object.fromEntries(cache); +}; + +const toFlagCacheEntry = ( + key: string, + flag: unknown +): FlagCacheEntry | null => { + // A malformed payload can carry a non-object flag (e.g. `flags: { "k": null }`). + // Skip the bad entry with a warning instead of throwing and aborting decoding of + // the whole configuration. + if (typeof flag !== 'object' || flag === null) { + InternalLog.log( + `Flag "${key}" is not an object. Omitting it from the configuration.`, + SdkVerbosity.WARN + ); + return null; + } + + const { + variationType, + variationValue, + variationKey, + allocationKey, + reason, + doLog, + extraLogging + } = flag as Partial; + + if ( + typeof variationType !== 'string' || + !SUPPORTED_VARIATION_TYPES.has(variationType) + ) { + InternalLog.log( + `Flag "${key}" has an unsupported variation type "${String( + variationType + )}". Omitting it from the configuration.`, + SdkVerbosity.WARN + ); + return null; + } + + if (!valueMatchesVariationType(variationValue, variationType)) { + InternalLog.log( + `Flag "${key}" value does not match its variation type "${variationType}". Omitting it from the configuration.`, + SdkVerbosity.WARN + ); + return null; + } + + // The remaining fields feed evaluation and native exposure tracking. A corrupt + // payload could carry wrong types here, so validate before forwarding them. + if ( + typeof allocationKey !== 'string' || + typeof variationKey !== 'string' || + typeof reason !== 'string' || + typeof doLog !== 'boolean' || + (extraLogging !== undefined && + (typeof extraLogging !== 'object' || extraLogging === null)) + ) { + InternalLog.log( + `Flag "${key}" has malformed metadata. Omitting it from the configuration.`, + SdkVerbosity.WARN + ); + return null; + } + + // `serialId` is intentionally not propagated: `FlagCacheEntry` has no slot for it + // and the native CDN-fetched snapshot omits it too, so dropping it keeps + // offline/online parity. + return { + key, + value: variationValue, + allocationKey, + variationKey, + variationType, + variationValue: stringifyValue(variationValue), + reason, + doLog, + extraLogging: extraLogging ?? {} + }; +}; + +const valueMatchesVariationType = ( + value: unknown, + variationType: string +): boolean => { + switch (variationType) { + case 'boolean': + return typeof value === 'boolean'; + case 'string': + return typeof value === 'string'; + case 'number': + case 'float': + // Reject NaN/Infinity: native parsers can't round-trip them. + return typeof value === 'number' && Number.isFinite(value); + case 'integer': + // A fractional value under an integer flag would be truncated/mis-parsed + // natively, so require a whole number. + return Number.isInteger(value); + case 'object': + // The `object` variation type carries arbitrary JSON — objects, arrays, + // numbers, strings, booleans, or null. ffe-service enforces a top-level + // object at the API layer, but that is not a storage constraint, so a + // payload could still carry any JSON value here. Accept it and rely on the + // value being defended downstream (string form + evaluation both tolerate it). + return true; + default: + return false; + } +}; + +/** + * Derive the string form of a flag value expected by native Android exposure tracking. + * Objects/arrays are JSON-encoded; everything else uses `String(...)`, which yields + * lowercase `"true"/"false"` for booleans. + */ +const stringifyValue = (value: unknown): string => { + if (value === null) { + return 'null'; + } + if (typeof value === 'object') { + return JSON.stringify(value); + } + return String(value); +}; diff --git a/packages/core/src/flags/configuration/types.ts b/packages/core/src/flags/configuration/types.ts new file mode 100644 index 000000000..0a21b8863 --- /dev/null +++ b/packages/core/src/flags/configuration/types.ts @@ -0,0 +1,61 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +import type { + FlagsConfiguration, + PrecomputedConfiguration, + PrecomputedConfigurationResponse as FlaggingCorePrecomputedConfigurationResponse, + PrecomputedFlag as FlaggingCorePrecomputedFlag +} from '@datadog/flagging-core'; + +/** + * The flag `variationType`s the decoder accepts. `@datadog/flagging-core` models the + * type as OpenFeature's `boolean | string | number | object`, and the CDN confirms only + * those are emitted. `integer`/`float` are kept here defensively — the decoder validates + * untrusted payloads and treats both as JavaScript `number`s. + */ +export const SUPPORTED_VARIATION_TYPES: ReadonlySet = new Set([ + 'boolean', + 'string', + 'number', + 'integer', + 'float', + 'object' +]); + +/** + * The context an evaluation is performed against, as it appears **on the wire**. + * + * This is the OpenFeature-shaped context: a flat object with an optional + * `targetingKey` and arbitrary sibling attributes. It is intentionally different + * from the SDK's internal `EvaluationContext` (`{ targetingKey, attributes }`); + * callers must normalize before comparing the two. + */ +export type WireEvaluationContext = { + targetingKey?: string; +} & Record; + +// The wire/precomputed types are re-exported from `@datadog/flagging-core` so this SDK +// shares the canonical shapes instead of maintaining its own copies. Local names are +// kept so the rest of the SDK is insulated from the upstream naming. +export type PrecomputedFlag = FlaggingCorePrecomputedFlag; +export type PrecomputedConfigurationResponse = FlaggingCorePrecomputedConfigurationResponse; +export type ParsedPrecomputedConfiguration = PrecomputedConfiguration; +export type ParsedFlagsConfiguration = FlagsConfiguration; + +/** + * The serialized `ConfigurationWire` envelope (version 1). `@datadog/flagging-core` + * keeps its own wire envelope internal, so this mirrors the shape for our local + * {@link configurationToString} (see wire.ts). + */ +export interface ConfigurationWire { + version: 1; + precomputed?: { + response: string; + context?: WireEvaluationContext; + fetchedAt?: number; + }; +} diff --git a/packages/core/src/flags/configuration/wire.ts b/packages/core/src/flags/configuration/wire.ts new file mode 100644 index 000000000..3608370cf --- /dev/null +++ b/packages/core/src/flags/configuration/wire.ts @@ -0,0 +1,44 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +import { configurationFromString } from '@datadog/flagging-core'; + +import type { ConfigurationWire, ParsedFlagsConfiguration } from './types'; + +// Parsing is reused from `@datadog/flagging-core` (the canonical wire implementation) +// rather than reimplemented here. It is lenient: it returns an empty configuration +// (`{}`) for malformed input or an unsupported wire version rather than throwing. +export { configurationFromString }; + +/** + * Serialize an in-memory {@link ParsedFlagsConfiguration} back into a portable + * `ConfigurationWire` string that `configurationFromString` can read. + * + * The serialized format is unspecified/opaque and may change between versions. + * + * TODO: replace this with `@datadog/flagging-core`'s `configurationToString` once the + * next major version (>= 2.0.0) lands. flagging-core 1.2.x has a broken serializer + * (it stringifies the whole `precomputed` object into `precomputed.response` instead of + * just `.response`, which double-nests the response and drops every flag on a + * serialize→parse round-trip — https://github.com/DataDog/openfeature-js-client/pull/331). + * Until the fix ships, we keep this correct local copy and depend on flagging-core only + * for `configurationFromString`. + */ +export const configurationToString = ( + configuration: ParsedFlagsConfiguration +): string => { + const wire: ConfigurationWire = { version: 1 }; + + if (configuration.precomputed) { + wire.precomputed = { + response: JSON.stringify(configuration.precomputed.response), + context: configuration.precomputed.context, + fetchedAt: configuration.precomputed.fetchedAt + }; + } + + return JSON.stringify(wire); +}; diff --git a/yarn.lock b/yarn.lock index 47f5dbc44..26788d988 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2722,6 +2722,15 @@ __metadata: languageName: node linkType: hard +"@datadog/flagging-core@npm:^1.2.1": + version: 1.2.1 + resolution: "@datadog/flagging-core@npm:1.2.1" + dependencies: + spark-md5: ^3.0.2 + checksum: 714e92b0fc43d9d14b79d9d235da0c97522332f3ba384ca8155b5c35817967286d012096282ee6975bf43d550358e750131ab0c3b00977dca2ffb0d5d9c3cd38 + languageName: node + linkType: hard + "@datadog/libdatadog@npm:^0.6.0": version: 0.6.0 resolution: "@datadog/libdatadog@npm:0.6.0" @@ -2863,6 +2872,8 @@ __metadata: version: 0.0.0-use.local resolution: "@datadog/mobile-react-native@workspace:packages/core" dependencies: + "@datadog/flagging-core": ^1.2.1 + "@openfeature/core": ^1.9.2 "@testing-library/react-native": 7.0.2 big-integer: ^1.6.52 react-native-builder-bob: 0.26.0 @@ -18126,6 +18137,13 @@ __metadata: languageName: node linkType: hard +"spark-md5@npm:^3.0.2": + version: 3.0.2 + resolution: "spark-md5@npm:3.0.2" + checksum: 5feebff0bfabcecf56ba03af3e38fdb068272ed41fbf0a94ff9ef65b9bb9cb1dd69be3684db6542e62497b1eac3ae324c07ac4dcb606465dc36ca048177077bf + languageName: node + linkType: hard + "spdx-correct@npm:^3.0.0": version: 3.2.0 resolution: "spdx-correct@npm:3.2.0"