Skip to content

chore: init shared openfeature server provider#1329

Open
joker23 wants to merge 1 commit intomainfrom
skz/sdk-2215/openfeature-server-common
Open

chore: init shared openfeature server provider#1329
joker23 wants to merge 1 commit intomainfrom
skz/sdk-2215/openfeature-server-common

Conversation

@joker23
Copy link
Copy Markdown
Contributor

@joker23 joker23 commented Apr 28, 2026

This PR will initialize the migration of openfeature node server provider to js-core. Specifically, this PR will create a shared openfeature server provider base class.

REVIEWER: The logic in this should be a more generalized version of https://github.com/launchdarkly/openfeature-node-server/tree/main.

Theoretically, this would allow us to implement openfeature providers pretty easily (eg https://github.com/launchdarkly/js-core/blob/skz/sdk-2213/openfeature-migration/packages/sdk/openfeature-node-server/src/LaunchDarklyProvider.ts)

NOTE: at the moment this code won't do anything until we implement the implementing provider in the next PR.


Note

Medium Risk
Adds a new provider foundation layer that will be reused by future SDKs; translation/typing logic and context handling bugs could affect flag evaluation and tracking once adopted.

Overview
Introduces a new shared workspace package, @launchdarkly/openfeature-js-server-common, to centralize LaunchDarkly’s server-side OpenFeature provider implementation.

The package adds BaseOpenFeatureProvider (initialization, typed resolve*Evaluation methods, OpenFeature events, and track bridging) plus utilities to translate OpenFeature evaluation context/results/tracking details into LaunchDarkly equivalents, with cycle detection and attribute validation covered by new Jest tests. Repo plumbing is updated to include the workspace, TypeScript project reference, and release-please configuration for pre-1.0 versioning.

Reviewed by Cursor Bugbot for commit a076b4f. Bugbot is set up for automated code reviews on this repo. Configure here.

@github-actions
Copy link
Copy Markdown
Contributor

@launchdarkly/js-sdk-common size report
This is the brotli compressed size of the ESM build.
Compressed size: 25623 bytes
Compressed size limit: 29000
Uncompressed size: 125843 bytes

@github-actions
Copy link
Copy Markdown
Contributor

@launchdarkly/js-client-sdk size report
This is the brotli compressed size of the ESM build.
Compressed size: 31840 bytes
Compressed size limit: 34000
Uncompressed size: 113634 bytes

@github-actions
Copy link
Copy Markdown
Contributor

@launchdarkly/browser size report
This is the brotli compressed size of the ESM build.
Compressed size: 179547 bytes
Compressed size limit: 200000
Uncompressed size: 830815 bytes

@github-actions
Copy link
Copy Markdown
Contributor

@launchdarkly/js-client-sdk-common size report
This is the brotli compressed size of the ESM build.
Compressed size: 38473 bytes
Compressed size limit: 39000
Uncompressed size: 211104 bytes

@joker23
Copy link
Copy Markdown
Contributor Author

joker23 commented Apr 28, 2026

@cursor review

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit a0956f5. Configure here.


const convertedContext: LDContextCommon = { key: finalKey };
Object.entries(inCommon).forEach(([key, value]) => {
if (key === 'targetingKey' || key === 'key') {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kind attribute leaks through spread, overriding finalKind

Medium Severity

translateContextCommon filters out targetingKey and key but not kind. For single-kind contexts, the kind attribute passes through convertAttributes and ends up in the returned object. Because the spread in { kind: finalKind, ...translateContextCommon(...) } comes second, the unfiltered kind overrides finalKind. This breaks the fallback to 'user' when kind is a non-string value — the invalid value leaks into the LDContext despite the warning at line 139. The multi-context path correctly filters kind at line 120, but translateContextCommon does not.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit a0956f5. Configure here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Existing bug... will track internally

object[key] = {};
Object.entries(value).forEach(([objectKey, objectValue]) => {
convertAttributes(logger, objectKey, objectValue, object[key], [...visited, value]);
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Null attribute values crash in convertAttributes

Medium Severity

In convertAttributes, when value is null, typeof null === 'object' is true and !Array.isArray(null) is true, so the code enters the object branch. It then calls Object.entries(null) on line 43, which throws an unhandled TypeError: Cannot convert undefined or null to object. Any custom attribute with a null value in the evaluation context (e.g., { targetingKey: 'key', myAttr: null }) will crash the provider. A null guard is needed before or within the typeof value === 'object' check.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit a0956f5. Configure here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Existing bug... will track internally

@joker23 joker23 marked this pull request as ready for review April 28, 2026 19:10
@joker23 joker23 requested a review from a team as a code owner April 28, 2026 19:10
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 potential issues.

View 4 additional findings in Devin Review.

Open in Devin Review

Comment on lines +86 to +87
if (key === 'targetingKey' || key === 'key') {
return;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 kind attribute not filtered in translateContextCommon, overrides corrected finalKind via object spread

When a single-kind evaluation context has a non-string kind (e.g., { kind: 42, targetingKey: 'foo' }), translateContext correctly detects this at line 139, logs a warning, and keeps finalKind = 'user'. However, translateContextCommon does not skip kind in its attribute iteration (line 86 only skips targetingKey and key), so kind flows through convertAttributes and is set on the returned convertedContext object. Back in translateContext at line 143-145, the spread { kind: finalKind, ...translateContextCommon(...) } causes the non-string kind from the spread to override the corrected finalKind. The resulting LDContext has an invalid non-string kind (e.g., kind: 42 instead of kind: 'user'), which will cause incorrect behavior in the LD SDK's flag evaluation.

Suggested change
if (key === 'targetingKey' || key === 'key') {
return;
if (key === 'targetingKey' || key === 'key' || key === 'kind') {
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

translateContext(this._logger, context),
defaultValue,
);
if (typeof res.value === 'object') {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 resolveObjectEvaluation treats null as a valid object due to typeof null === 'object'

At BaseOpenFeatureProvider.ts:174, typeof res.value === 'object' is used to check whether the LD SDK returned an object. In JavaScript, typeof null === 'object' evaluates to true, so if the SDK returns null (e.g., for a JSON flag whose value is null), the check passes and null is returned as a successful object resolution. The caller of resolveObjectEvaluation expects an object value but receives null, which violates the OpenFeature contract. The fix should add a null guard: res.value !== null && typeof res.value === 'object'.

Suggested change
if (typeof res.value === 'object') {
if (res.value !== null && typeof res.value === 'object') {
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@joker23 joker23 force-pushed the skz/sdk-2215/openfeature-server-common branch from a0956f5 to a076b4f Compare April 29, 2026 18:35
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 8 additional findings in Devin Review.

Open in Devin Review

Comment on lines +40 to +44
} else if (typeof value === 'object' && !Array.isArray(value)) {
// eslint-disable-next-line no-param-reassign
object[key] = {};
Object.entries(value).forEach(([objectKey, objectValue]) => {
convertAttributes(logger, objectKey, objectValue, object[key], [...visited, value]);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 convertAttributes crashes with TypeError when attribute value is null

typeof null === 'object' is true in JavaScript, and !Array.isArray(null) is also true. When a custom attribute has a null value (which is a valid EvaluationContextValue — it's included in OpenFeature's PrimitiveValue type), the code at line 40 enters the object branch and calls Object.entries(null) at line 43, which throws a TypeError: Cannot convert undefined or null to object. For example, translateContext(logger, { targetingKey: 'key', someAttr: null }) will crash. The same typeof null === 'object' issue also affects the multi-context path at translateContext.ts:122 — a null sub-context value would enter the object branch and crash when accessing valueRecord.targetingKey.

Suggested change
} else if (typeof value === 'object' && !Array.isArray(value)) {
// eslint-disable-next-line no-param-reassign
object[key] = {};
Object.entries(value).forEach(([objectKey, objectValue]) => {
convertAttributes(logger, objectKey, objectValue, object[key], [...visited, value]);
} else if (value != null && typeof value === 'object' && !Array.isArray(value)) {
// eslint-disable-next-line no-param-reassign
object[key] = {};
Object.entries(value).forEach(([objectKey, objectValue]) => {
convertAttributes(logger, objectKey, objectValue, object[key], [...visited, value]);
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant