diff --git a/packages/openfeature-node-provider/README.md b/packages/openfeature-node-provider/README.md index f9183534..75699165 100644 --- a/packages/openfeature-node-provider/README.md +++ b/packages/openfeature-node-provider/README.md @@ -4,26 +4,23 @@ The official OpenFeature Node.js provider for [Bucket](https://bucket.co) featur ## Installation -``` -$ npm install @bucketco/openfeature-node-provider +```shell +npm install @bucketco/openfeature-node-provider ``` -#### Required peer dependencies +### Required peer dependencies The OpenFeature SDK is required as peer dependency. - The minimum required version of `@openfeature/server-sdk` currently is `1.13.5`. - The minimum required version of `@bucketco/node-sdk` currently is `2.0.0`. -``` -$ npm install @openfeature/server-sdk @bucketco/node-sdk +```shell +npm install @openfeature/server-sdk @bucketco/node-sdk ``` ## Usage The provider uses the [Bucket Node.js SDK](https://docs.bucket.co/quickstart/supported-languages-frameworks/node.js-sdk). - The available options can be found in the [Bucket Node.js SDK](https://github.com/bucketco/bucket-javascript-sdk/tree/main/packages/node-sdk#initialization-options). ### Example using the default configuration @@ -56,6 +53,59 @@ const enterpriseFeatureEnabled = await client.getBooleanValue( ); ``` +## Feature resolution methods + +The Bucket OpenFeature Provider implements the OpenFeature evaluation interface for different value types. Each method handles the resolution of feature flags according to the OpenFeature specification. + +### Common behavior + +All resolution methods share these behaviors: + +- Return default value with `PROVIDER_NOT_READY` if client is not initialized, +- Return default value with `FLAG_NOT_FOUND` if flag doesn't exist, +- Return default value with `ERROR` if there was a type mismatch, +- Return evaluated value with `TARGETING_MATCH` on successful resolution. + +### Type-Specific Methods + +#### Boolean Resolution + +```ts +client.getBooleanValue("my-flag", false); +``` + +Returns the feature's enabled state. This is the most common use case for feature flags. + +#### String Resolution + +```ts +client.getStringValue("my-flag", "default"); +``` + +Returns the feature's remote config key (also known as "variant"). Useful for multi-variate use cases. + +#### Number Resolution + +```ts +client.getNumberValue("my-flag", 0); +``` + +Not directly supported by Bucket. Use `getObjectValue` instead for numeric configurations. + +#### Object Resolution + +```ts +// works for any type: +client.getObjectValue("my-flag", { defaultValue: true }); +client.getObjectValue("my-flag", "string-value"); +client.getObjectValue("my-flag", 199); +``` + +Returns the feature's remote config payload with type validation. This is the most flexible method, +allowing for complex configuration objects or simple types. + +The object resolution performs runtime type checking between the default value and the feature payload to ensure type safety. + ## Translating Evaluation Context Bucket uses a context object of the following shape: @@ -69,11 +119,19 @@ export type BucketContext = { /** * The user context. If the user is set, the user ID is required. */ - user?: { id: string; [k: string]: any }; + user?: { + id: string; + name?: string; + email?: string; + avatar?: string; + [k: string]: any; + }; + /** * The company context. If the company is set, the company ID is required. */ - company?: { id: string; [k: string]: any }; + company?: { id: string; name?: string; avatar?: string; [k: string]: any }; + /** * The other context. This is used for any additional context that is not related to user or company. */ @@ -91,14 +149,17 @@ import { BucketNodeProvider } from "@openfeature/bucket-node-provider"; const contextTranslator = (context: EvaluationContext): BucketContext => { return { user: { - id: context.targetingKey, + id: context.targetingKey ?? context["userId"]?.toString(), name: context["name"]?.toString(), email: context["email"]?.toString(), + avatar: context["avatar"]?.toString(), country: context["country"]?.toString(), }, company: { - id: context["companyId"], - name: context["companyName"], + id: context["companyId"]?.toString(), + name: context["companyName"]?.toString(), + avatar: context["companyAvatar"]?.toString(), + plan: context["companyPlan"]?.toString(), }, }; }; @@ -115,7 +176,7 @@ It's straight forward to start sending tracking events through OpenFeature. Simply call the "track" method on the OpenFeature client: -```ts +```typescript import { BucketNodeProvider } from "@bucketco/openfeature-node-provider"; import { OpenFeature } from "@openfeature/server-sdk"; @@ -132,8 +193,7 @@ const enterpriseFeatureEnabled = await client.track( ); ``` -# License - -MIT License +## License -Copyright (c) 2025 Bucket ApS +> MIT License +> Copyright (c) 2025 Bucket ApS diff --git a/packages/openfeature-node-provider/package.json b/packages/openfeature-node-provider/package.json index 06eca2f5..5cf1a21a 100644 --- a/packages/openfeature-node-provider/package.json +++ b/packages/openfeature-node-provider/package.json @@ -1,6 +1,6 @@ { "name": "@bucketco/openfeature-node-provider", - "version": "0.2.1", + "version": "0.3.0-alpha.0", "license": "MIT", "repository": { "type": "git", @@ -44,7 +44,7 @@ "vitest": "~1.6.0" }, "dependencies": { - "@bucketco/node-sdk": ">=1.4.2" + "@bucketco/node-sdk": "1.6.0-alpha.3" }, "peerDependencies": { "@openfeature/server-sdk": ">=1.16.1" diff --git a/packages/openfeature-node-provider/src/index.test.ts b/packages/openfeature-node-provider/src/index.test.ts index 8f506df1..9d2b63b9 100644 --- a/packages/openfeature-node-provider/src/index.test.ts +++ b/packages/openfeature-node-provider/src/index.test.ts @@ -1,9 +1,9 @@ -import { ErrorCode } from "@openfeature/core"; -import { beforeAll, beforeEach, describe, expect, it, Mock, vi } from "vitest"; +import { ProviderStatus } from "@openfeature/server-sdk"; +import { beforeEach, describe, expect, it, Mock, vi } from "vitest"; import { BucketClient } from "@bucketco/node-sdk"; -import { BucketNodeProvider } from "./index"; +import { BucketNodeProvider, defaultContextTranslator } from "./index"; vi.mock("@bucketco/node-sdk", () => { const actualModule = vi.importActual("@bucketco/node-sdk"); @@ -17,6 +17,7 @@ vi.mock("@bucketco/node-sdk", () => { const bucketClientMock = { getFeatures: vi.fn(), + getFeature: vi.fn(), initialize: vi.fn().mockResolvedValue({}), flush: vi.fn(), track: vi.fn(), @@ -35,6 +36,8 @@ const bucketContext = { company: { id: "99" }, }; +const testFlagKey = "a-key"; + beforeEach(() => { vi.clearAllMocks(); }); @@ -42,121 +45,305 @@ beforeEach(() => { describe("BucketNodeProvider", () => { let provider: BucketNodeProvider; - const newBucketClient = BucketClient as Mock; - newBucketClient.mockReturnValue(bucketClientMock); + const mockBucketClient = BucketClient as Mock; + mockBucketClient.mockReturnValue(bucketClientMock); + + let mockTranslatorFn: Mock; + + function mockFeature( + enabled: boolean, + configKey?: string | null, + configPayload?: any, + ) { + const config = { + key: configKey, + payload: configPayload, + }; + + bucketClientMock.getFeature = vi.fn().mockReturnValue({ + isEnabled: enabled, + config, + }); + + bucketClientMock.getFeatures = vi.fn().mockReturnValue({ + [testFlagKey]: { + isEnabled: enabled, + config: { + key: "key", + payload: configPayload, + }, + }, + }); + } - const translatorFn = vi.fn().mockReturnValue(bucketContext); + beforeEach(async () => { + mockTranslatorFn = vi.fn().mockReturnValue(bucketContext); - beforeAll(async () => { provider = new BucketNodeProvider({ secretKey, - contextTranslator: translatorFn, + contextTranslator: mockTranslatorFn, }); + await provider.initialize(); }); - it("calls the constructor", () => { - provider = new BucketNodeProvider({ - secretKey, - contextTranslator: translatorFn, + describe("contextTranslator", () => { + it("defaultContextTranslator provides the correct context", async () => { + expect( + defaultContextTranslator({ + userId: 123, + name: "John Doe", + email: "ron@bucket.co", + avatar: "https://bucket.co/avatar.png", + companyId: "456", + companyName: "Acme, Inc.", + companyAvatar: "https://acme.com/company-avatar.png", + companyPlan: "pro", + }), + ).toEqual({ + user: { + id: "123", + name: "John Doe", + email: "ron@bucket.co", + avatar: "https://bucket.co/avatar.png", + }, + company: { + id: "456", + name: "Acme, Inc.", + plan: "pro", + avatar: "https://acme.com/company-avatar.png", + }, + }); + }); + + it("defaultContextTranslator uses targetingKey if provided", async () => { + expect( + defaultContextTranslator({ + targetingKey: "123", + }), + ).toMatchObject({ + user: { + id: "123", + }, + company: { + id: undefined, + }, + }); }); - expect(newBucketClient).toHaveBeenCalledTimes(1); - expect(newBucketClient).toHaveBeenCalledWith({ secretKey }); }); - it("uses the contextTranslator function", async () => { - const track = vi.fn(); - bucketClientMock.getFeatures.mockReturnValue({ - booleanTrue: { - isEnabled: true, - key: "booleanTrue", - track, - }, + describe("lifecycle", () => { + it("calls the constructor of BucketClient", () => { + mockBucketClient.mockClear(); + + provider = new BucketNodeProvider({ + secretKey, + contextTranslator: mockTranslatorFn, + }); + + expect(mockBucketClient).toHaveBeenCalledTimes(1); + expect(mockBucketClient).toHaveBeenCalledWith({ secretKey }); }); - await provider.resolveBooleanEvaluation("booleanTrue", false, context); - expect(translatorFn).toHaveBeenCalledTimes(1); - expect(translatorFn).toHaveBeenCalledWith(context); - expect(bucketClientMock.getFeatures).toHaveBeenCalledTimes(1); - expect(bucketClientMock.getFeatures).toHaveBeenCalledWith(bucketContext); + it("should set the status to READY if initialization succeeds", async () => { + provider = new BucketNodeProvider({ + secretKey, + contextTranslator: mockTranslatorFn, + }); + + await provider.initialize(); + + expect(provider.status).toBe(ProviderStatus.READY); + }); + + it("should keep the status as READY after closing", async () => { + provider = new BucketNodeProvider({ + secretKey: "invalid", + contextTranslator: mockTranslatorFn, + }); + + await provider.initialize(); + await provider.onClose(); + + expect(provider.status).toBe(ProviderStatus.READY); + }); + + it("calls flush when provider is closed", async () => { + await provider.onClose(); + expect(bucketClientMock.flush).toHaveBeenCalledTimes(1); + }); + + it("uses the contextTranslator function", async () => { + mockFeature(true); + + await provider.resolveBooleanEvaluation(testFlagKey, false, context); + + expect(mockTranslatorFn).toHaveBeenCalledTimes(1); + expect(mockTranslatorFn).toHaveBeenCalledWith(context); + + expect(bucketClientMock.getFeatures).toHaveBeenCalledTimes(1); + expect(bucketClientMock.getFeatures).toHaveBeenCalledWith(bucketContext); + }); }); - describe("method resolveBooleanEvaluation", () => { - it("should return right value if key exists", async () => { - const result = await provider.resolveBooleanEvaluation( - "booleanTrue", - false, - context, - ); - expect(result.value).toEqual(true); - expect(result.errorCode).toBeUndefined(); + describe("resolving flags", () => { + beforeEach(async () => { + await provider.initialize(); }); - it("should return the default value if key does not exists", async () => { - const result = await provider.resolveBooleanEvaluation( - "non-existent", + it("returns error if provider is not initialized", async () => { + provider = new BucketNodeProvider({ + secretKey: "invalid", + contextTranslator: mockTranslatorFn, + }); + + const val = await provider.resolveBooleanEvaluation( + testFlagKey, true, context, ); - expect(result.value).toEqual(true); - expect(result.errorCode).toEqual(ErrorCode.FLAG_NOT_FOUND); + + expect(val).toMatchObject({ + reason: "ERROR", + errorCode: "PROVIDER_NOT_READY", + value: true, + }); }); - }); - describe("method resolveNumberEvaluation", () => { - it("should return the default value and an error message", async () => { - const result = await provider.resolveNumberEvaluation("number1", 42); - expect(result.value).toEqual(42); - expect(result.errorCode).toEqual(ErrorCode.GENERAL); - expect(result.errorMessage).toEqual( - `Bucket doesn't support number flags`, + it("returns error if flag is not found", async () => { + mockFeature(true, "key", true); + const val = await provider.resolveBooleanEvaluation( + "missing-key", + true, + context, ); + + expect(val).toMatchObject({ + reason: "ERROR", + errorCode: "FLAG_NOT_FOUND", + value: true, + }); }); - }); - describe("method resolveStringEvaluation", () => { - it("should return the default value and an error message", async () => { - const result = await provider.resolveStringEvaluation( - "number1", - "defaultValue", + it("calls the client correctly when evaluating", async () => { + mockFeature(true, "key", true); + + const val = await provider.resolveBooleanEvaluation( + testFlagKey, + false, + context, ); - expect(result.value).toEqual("defaultValue"); - expect(result.errorCode).toEqual(ErrorCode.GENERAL); - expect(result.errorMessage).toEqual( - `Bucket doesn't support string flags`, + + expect(val).toMatchObject({ + reason: "TARGETING_MATCH", + value: true, + }); + + expect(bucketClientMock.getFeatures).toHaveBeenCalled(); + expect(bucketClientMock.getFeature).toHaveBeenCalledWith( + bucketContext, + testFlagKey, ); }); - }); - describe("method resolveObjectEvaluation", () => { - it("should return the default value and an error message", async () => { - const defaultValue = { key: "value" }; - const result = await provider.resolveObjectEvaluation( - "number1", - defaultValue, - ); - expect(result.value).toEqual(defaultValue); - expect(result.errorCode).toEqual(ErrorCode.GENERAL); - expect(result.errorMessage).toEqual( - `Bucket doesn't support object flags`, - ); + + it.each([ + [true, false, true, "TARGETING_MATCH", undefined], + [undefined, true, true, "ERROR", "FLAG_NOT_FOUND"], + [undefined, false, false, "ERROR", "FLAG_NOT_FOUND"], + ])( + "should return the correct result when evaluating boolean. enabled: %s, value: %s, default: %s, expected: %s, reason: %s, errorCode: %s`", + async (enabled, def, expected, reason, errorCode) => { + const configKey = enabled !== undefined ? "variant-1" : undefined; + + mockFeature(enabled ?? false, configKey); + const flagKey = enabled ? testFlagKey : "missing-key"; + + expect( + await provider.resolveBooleanEvaluation(flagKey, def, context), + ).toMatchObject({ + reason, + value: expected, + ...(configKey ? { variant: configKey } : {}), + ...(errorCode ? { errorCode } : {}), + }); + }, + ); + + it("should return error when context is missing user ID", async () => { + mockTranslatorFn.mockReturnValue({ user: {} }); + + expect( + await provider.resolveBooleanEvaluation(testFlagKey, true, context), + ).toMatchObject({ + reason: "ERROR", + errorCode: "INVALID_CONTEXT", + value: true, + }); }); - }); - describe("onClose", () => { - it("calls flush", async () => { - await provider.onClose(); - expect(bucketClientMock.flush).toHaveBeenCalledTimes(1); + it("should return error when evaluating number", async () => { + expect( + await provider.resolveNumberEvaluation(testFlagKey, 1), + ).toMatchObject({ + reason: "ERROR", + errorCode: "GENERAL", + value: 1, + }); }); + + it.each([ + ["key-1", "default", "key-1", "TARGETING_MATCH"], + [null, "default", "default", "DEFAULT"], + [undefined, "default", "default", "DEFAULT"], + ])( + "should return the correct result when evaluating string. variant: %s, def: %s, expected: %s, reason: %s, errorCode: %s`", + async (variant, def, expected, reason) => { + mockFeature(true, variant, {}); + expect( + await provider.resolveStringEvaluation(testFlagKey, def, context), + ).toMatchObject({ + reason, + value: expected, + ...(variant ? { variant } : {}), + }); + }, + ); + + it.each([ + [{}, { a: 1 }, {}, "TARGETING_MATCH", undefined], + ["string", "default", "string", "TARGETING_MATCH", undefined], + [15, -15, 15, "TARGETING_MATCH", undefined], + [true, false, true, "TARGETING_MATCH", undefined], + [null, { a: 2 }, { a: 2 }, "ERROR", "TYPE_MISMATCH"], + [100, "string", "string", "ERROR", "TYPE_MISMATCH"], + [true, 1337, 1337, "ERROR", "TYPE_MISMATCH"], + ["string", 1337, 1337, "ERROR", "TYPE_MISMATCH"], + [undefined, "default", "default", "ERROR", "TYPE_MISMATCH"], + ])( + "should return the correct result when evaluating object. payload: %s, default: %s, expected: %s, reason: %s, errorCode: %s`", + async (value, def, expected, reason, errorCode) => { + const configKey = value === undefined ? undefined : "config-key"; + mockFeature(true, configKey, value); + expect( + await provider.resolveObjectEvaluation(testFlagKey, def, context), + ).toMatchObject({ + reason, + value: expected, + ...(errorCode ? { errorCode, variant: configKey } : {}), + }); + }, + ); }); describe("track", () => { it("should track", async () => { - expect(translatorFn).toHaveBeenCalledTimes(0); + expect(mockTranslatorFn).toHaveBeenCalledTimes(0); provider.track("event", context, { action: "click", }); - expect(translatorFn).toHaveBeenCalledTimes(1); - expect(translatorFn).toHaveBeenCalledWith(context); + + expect(mockTranslatorFn).toHaveBeenCalledTimes(1); + expect(mockTranslatorFn).toHaveBeenCalledWith(context); expect(bucketClientMock.track).toHaveBeenCalledTimes(1); expect(bucketClientMock.track).toHaveBeenCalledWith("42", "event", { attributes: { action: "click" }, diff --git a/packages/openfeature-node-provider/src/index.ts b/packages/openfeature-node-provider/src/index.ts index 7f2f512a..ad771095 100644 --- a/packages/openfeature-node-provider/src/index.ts +++ b/packages/openfeature-node-provider/src/index.ts @@ -21,17 +21,22 @@ type ProviderOptions = ClientOptions & { contextTranslator?: (context: EvaluationContext) => BucketContext; }; -const defaultTranslator = (context: EvaluationContext): BucketContext => { +export const defaultContextTranslator = ( + context: EvaluationContext, +): BucketContext => { const user = { - id: context.targetingKey ?? context["id"]?.toString(), + id: context.targetingKey ?? context["userId"]?.toString(), name: context["name"]?.toString(), email: context["email"]?.toString(), + avatar: context["avatar"]?.toString(), country: context["country"]?.toString(), }; const company = { id: context["companyId"]?.toString(), name: context["companyName"]?.toString(), + avatar: context["companyAvatar"]?.toString(), + plan: context["companyPlan"]?.toString(), }; return { @@ -61,7 +66,7 @@ export class BucketNodeProvider implements Provider { constructor({ contextTranslator, ...opts }: ProviderOptions) { this._client = new BucketClient(opts); - this.contextTranslator = contextTranslator ?? defaultTranslator; + this.contextTranslator = contextTranslator ?? defaultContextTranslator; } public async initialize(): Promise { @@ -69,42 +74,90 @@ export class BucketNodeProvider implements Provider { this.status = ServerProviderStatus.READY; } - resolveBooleanEvaluation( + private resolveFeature( flagKey: string, - defaultValue: boolean, - context: EvaluationContext, - ): Promise> { - const features = this._client.getFeatures(this.contextTranslator(context)); + defaultValue: T, + context: BucketContext, + resolveFn: ( + feature: ReturnType, + ) => Promise>, + ): Promise> { + if (this.status !== ServerProviderStatus.READY) { + return Promise.resolve({ + value: defaultValue, + reason: StandardResolutionReasons.ERROR, + errorCode: ErrorCode.PROVIDER_NOT_READY, + errorMessage: "Bucket client not initialized", + }); + } - const feature = features[flagKey]; - if (!feature) { + if (!context.user?.id) { return Promise.resolve({ value: defaultValue, - source: "bucket-node", - flagKey, - errorCode: ErrorCode.FLAG_NOT_FOUND, reason: StandardResolutionReasons.ERROR, + errorCode: ErrorCode.INVALID_CONTEXT, + errorMessage: "At least a user ID is required", }); } + const features = this._client.getFeatures(context); + if (flagKey in features) { + return resolveFn(this._client.getFeature(context, flagKey)); + } + return Promise.resolve({ - value: feature.isEnabled, - source: "bucket-node", - flagKey, - reason: StandardResolutionReasons.TARGETING_MATCH, + value: defaultValue, + reason: StandardResolutionReasons.ERROR, + errorCode: ErrorCode.FLAG_NOT_FOUND, + errorMessage: `Flag ${flagKey} not found`, }); } + + resolveBooleanEvaluation( + flagKey: string, + defaultValue: boolean, + context: EvaluationContext, + ): Promise> { + return this.resolveFeature( + flagKey, + defaultValue, + this.contextTranslator(context), + (feature) => { + return Promise.resolve({ + value: feature.isEnabled, + variant: feature.config?.key, + reason: StandardResolutionReasons.TARGETING_MATCH, + }); + }, + ); + } + resolveStringEvaluation( - _flagKey: string, + flagKey: string, defaultValue: string, + context: EvaluationContext, ): Promise> { - return Promise.resolve({ - value: defaultValue, - reason: StandardResolutionReasons.ERROR, - errorCode: ErrorCode.GENERAL, - errorMessage: "Bucket doesn't support string flags", - }); + return this.resolveFeature( + flagKey, + defaultValue, + this.contextTranslator(context), + (feature) => { + if (!feature.config.key) { + return Promise.resolve({ + value: defaultValue, + reason: StandardResolutionReasons.DEFAULT, + }); + } + + return Promise.resolve({ + value: feature.config.key as string, + variant: feature.config.key, + reason: StandardResolutionReasons.TARGETING_MATCH, + }); + }, + ); } + resolveNumberEvaluation( _flagKey: string, defaultValue: number, @@ -113,19 +166,45 @@ export class BucketNodeProvider implements Provider { value: defaultValue, reason: StandardResolutionReasons.ERROR, errorCode: ErrorCode.GENERAL, - errorMessage: "Bucket doesn't support number flags", + errorMessage: + "Bucket doesn't support this method. Use `resolveObjectEvaluation` instead.", }); } + resolveObjectEvaluation( - _flagKey: string, + flagKey: string, defaultValue: T, + context: EvaluationContext, ): Promise> { - return Promise.resolve({ - value: defaultValue, - reason: StandardResolutionReasons.ERROR, - errorCode: ErrorCode.GENERAL, - errorMessage: "Bucket doesn't support object flags", - }); + return this.resolveFeature( + flagKey, + defaultValue, + this.contextTranslator(context), + (feature) => { + const expType = typeof defaultValue; + const payloadType = typeof feature.config.payload; + + if ( + feature.config.payload === undefined || + feature.config.payload === null || + payloadType !== expType + ) { + return Promise.resolve({ + value: defaultValue, + variant: feature.config.key, + reason: StandardResolutionReasons.ERROR, + errorCode: ErrorCode.TYPE_MISMATCH, + errorMessage: `Expected remote config payload of type \`${expType}\` but got \`${payloadType}\`.`, + }); + } + + return Promise.resolve({ + value: feature.config.payload, + variant: feature.config.key, + reason: StandardResolutionReasons.TARGETING_MATCH, + }); + }, + ); } track( diff --git a/yarn.lock b/yarn.lock index 7f1ce407..259b2b5e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -948,12 +948,12 @@ __metadata: languageName: unknown linkType: soft -"@bucketco/node-sdk@npm:>=1.4.2": - version: 1.5.0 - resolution: "@bucketco/node-sdk@npm:1.5.0" +"@bucketco/node-sdk@npm:1.6.0-alpha.3": + version: 1.6.0-alpha.3 + resolution: "@bucketco/node-sdk@npm:1.6.0-alpha.3" dependencies: "@bucketco/flag-evaluation": "npm:~0.1.0" - checksum: 10c0/63230400c0c0fa6ccf8708550bbcf583cc58bd18a2b99e19ec1dde43bce593c43136790ff3f0573f171c123c6a0555eebafcefdfa5cc71a2e706079fdb1ebe39 + checksum: 10c0/7f9654168a3a94b7971d1d2ca0736f9ea1fd1f68d2f170b3eccc345eeab34a1bcb5da7973a63b38b4f0fb8df1353143d87d24c85bca2d52fe6fc0aaf9f2a4951 languageName: node linkType: hard @@ -1007,7 +1007,7 @@ __metadata: dependencies: "@babel/core": "npm:~7.24.7" "@bucketco/eslint-config": "npm:~0.0.2" - "@bucketco/node-sdk": "npm:>=1.4.2" + "@bucketco/node-sdk": "npm:1.6.0-alpha.3" "@bucketco/tsconfig": "npm:~0.0.2" "@openfeature/core": "npm:^1.5.0" "@openfeature/server-sdk": "npm:>=1.16.1"