@beignet/provider-flags-openfeature 0.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,14 @@
1
+ # @beignet/provider-flags-openfeature
2
+
3
+ ## 0.0.14
4
+
5
+ ### Patch Changes
6
+
7
+ - 6828442: Add first-class feature flag primitives, an OpenFeature-backed flags provider,
8
+ and devtools support for flag evaluation, exposure, and tracking events.
9
+
10
+ ## 0.0.13
11
+
12
+ ### Patch Changes
13
+
14
+ - Initial OpenFeature-backed feature flags provider.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Taylor Bryant
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,68 @@
1
+ # @beignet/provider-flags-openfeature
2
+
3
+ > [!CAUTION]
4
+ > Beignet is experimental alpha software. The `0.0.x` package line is for early
5
+ > evaluation, and APIs may change between releases while the framework settles.
6
+
7
+ OpenFeature-backed `FlagsPort` provider for Beignet applications.
8
+
9
+ The provider installs `ctx.ports.flags` using
10
+ [`@openfeature/server-sdk`](https://openfeature.dev/) and keeps OpenFeature
11
+ types out of Beignet core.
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ bun add @beignet/provider-flags-openfeature @openfeature/server-sdk
17
+ ```
18
+
19
+ Install and configure your chosen OpenFeature provider package separately.
20
+
21
+ ## Setup
22
+
23
+ ```typescript
24
+ import { createNextServer } from "@beignet/next";
25
+ import { createOpenFeatureFlagsProvider } from "@beignet/provider-flags-openfeature";
26
+ import { appPorts } from "@/infra/app-ports";
27
+ import { openFeatureProvider } from "@/infra/flags/openfeature";
28
+
29
+ export const server = await createNextServer({
30
+ ports: appPorts,
31
+ providers: [
32
+ createOpenFeatureFlagsProvider({
33
+ provider: openFeatureProvider,
34
+ domain: "app",
35
+ }),
36
+ ],
37
+ context: appContextBlueprint,
38
+ });
39
+ ```
40
+
41
+ The provider contributes:
42
+
43
+ - `ctx.ports.flags`, the standard Beignet `FlagsPort`
44
+ - `ctx.ports.openFeature`, an escape hatch with the raw OpenFeature client/API
45
+
46
+ ## Direct port factory
47
+
48
+ Use `createOpenFeatureFlags(...)` when your app already owns an OpenFeature
49
+ client:
50
+
51
+ ```typescript
52
+ import { createOpenFeatureFlags } from "@beignet/provider-flags-openfeature";
53
+
54
+ const flags = createOpenFeatureFlags({ client });
55
+ ```
56
+
57
+ ## Failure behavior
58
+
59
+ OpenFeature evaluation errors return the Beignet flag default with
60
+ `reason: "error"` in `details(...)`. Programmer errors, such as malformed flag
61
+ definitions, remain application errors.
62
+
63
+ ## Devtools
64
+
65
+ When `@beignet/devtools` is installed before this provider, flag evaluations,
66
+ provider errors, explicit exposures, and tracking events appear under the
67
+ Feature flags watcher. Private attributes are redacted from instrumentation
68
+ details.
@@ -0,0 +1,96 @@
1
+ /**
2
+ * @beignet/provider-flags-openfeature
3
+ *
4
+ * OpenFeature-backed feature flags provider for Beignet.
5
+ */
6
+ import type { FlagEvaluationContext, FlagEvaluationDetails, FlagEvaluationOptions, FlagExposureOptions, FlagsPort, FlagTrackOptions } from "@beignet/core/flags";
7
+ import { type ProviderInstrumentationTarget } from "@beignet/core/providers";
8
+ import { type Client, OpenFeature, type Provider } from "@openfeature/server-sdk";
9
+ /**
10
+ * Ports contributed by the OpenFeature flags provider.
11
+ */
12
+ export interface OpenFeatureFlagsProviderPorts {
13
+ /**
14
+ * Beignet feature flag port backed by OpenFeature.
15
+ */
16
+ flags: FlagsPort;
17
+ /**
18
+ * Escape hatch for advanced OpenFeature usage.
19
+ */
20
+ openFeature: OpenFeatureEscapeHatch;
21
+ }
22
+ /**
23
+ * Escape hatch for apps that need the raw OpenFeature client/API.
24
+ */
25
+ export interface OpenFeatureEscapeHatch {
26
+ client: Client;
27
+ api: typeof OpenFeature;
28
+ }
29
+ /**
30
+ * Options for the direct OpenFeature flags port factory.
31
+ */
32
+ export interface CreateOpenFeatureFlagsOptions {
33
+ client: Client;
34
+ instrumentation?: ProviderInstrumentationTarget;
35
+ }
36
+ /**
37
+ * Options for the OpenFeature provider package.
38
+ */
39
+ export interface CreateOpenFeatureFlagsProviderOptions {
40
+ /**
41
+ * Existing OpenFeature client. When supplied, setup does not register a
42
+ * provider with the singleton OpenFeature API.
43
+ */
44
+ client?: Client;
45
+ /**
46
+ * OpenFeature provider to install during Beignet server setup.
47
+ */
48
+ provider?: Provider;
49
+ /**
50
+ * Optional OpenFeature domain for provider and client binding.
51
+ */
52
+ domain?: string;
53
+ /**
54
+ * Optional client version used when creating a domain-scoped client.
55
+ */
56
+ version?: string;
57
+ /**
58
+ * Await provider initialization during setup.
59
+ *
60
+ * @default true
61
+ */
62
+ waitForReady?: boolean;
63
+ /**
64
+ * Clear OpenFeature providers when the Beignet server stops.
65
+ *
66
+ * @default false
67
+ */
68
+ clearOnStop?: boolean;
69
+ }
70
+ /**
71
+ * Create a Beignet FlagsPort backed by an OpenFeature client.
72
+ */
73
+ export declare function createOpenFeatureFlags(options: CreateOpenFeatureFlagsOptions): FlagsPort;
74
+ /**
75
+ * Create a Beignet provider that contributes `ports.flags`.
76
+ */
77
+ export declare function createOpenFeatureFlagsProvider(options?: CreateOpenFeatureFlagsProviderOptions): import("@beignet/core/providers").ServiceProvider<unknown, import("@standard-schema/spec").StandardSchemaV1<void, void>, {
78
+ flags: FlagsPort;
79
+ openFeature: {
80
+ client: Client;
81
+ api: import("@openfeature/server-sdk").OpenFeatureAPI;
82
+ };
83
+ }, unknown, void>;
84
+ /**
85
+ * Ready-to-register OpenFeature flags provider using the default OpenFeature
86
+ * singleton client.
87
+ */
88
+ export declare const openFeatureFlagsProvider: import("@beignet/core/providers").ServiceProvider<unknown, import("@standard-schema/spec").StandardSchemaV1<void, void>, {
89
+ flags: FlagsPort;
90
+ openFeature: {
91
+ client: Client;
92
+ api: import("@openfeature/server-sdk").OpenFeatureAPI;
93
+ };
94
+ }, unknown, void>;
95
+ export type { FlagEvaluationContext, FlagEvaluationDetails, FlagEvaluationOptions, FlagExposureOptions, FlagsPort, FlagTrackOptions, };
96
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAEV,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,mBAAmB,EACnB,SAAS,EACT,gBAAgB,EAEjB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAGL,KAAK,6BAA6B,EACnC,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,KAAK,MAAM,EAIX,WAAW,EACX,KAAK,QAAQ,EAEd,MAAM,yBAAyB,CAAC;AAEjC;;GAEG;AACH,MAAM,WAAW,6BAA6B;IAC5C;;OAEG;IACH,KAAK,EAAE,SAAS,CAAC;IACjB;;OAEG;IACH,WAAW,EAAE,sBAAsB,CAAC;CACrC;AAED;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,OAAO,WAAW,CAAC;CACzB;AAED;;GAEG;AACH,MAAM,WAAW,6BAA6B;IAC5C,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,CAAC,EAAE,6BAA6B,CAAC;CACjD;AAED;;GAEG;AACH,MAAM,WAAW,qCAAqC;IACpD;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;OAEG;IACH,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAqGD;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,6BAA6B,GACrC,SAAS,CA4HX;AA8DD;;GAEG;AACH,wBAAgB,8BAA8B,CAC5C,OAAO,GAAE,qCAA0C;;;;;;kBAgCpD;AAED;;;GAGG;AACH,eAAO,MAAM,wBAAwB;;;;;;iBAAmC,CAAC;AAEzE,YAAY,EACV,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,mBAAmB,EACnB,SAAS,EACT,gBAAgB,GACjB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,265 @@
1
+ /**
2
+ * @beignet/provider-flags-openfeature
3
+ *
4
+ * OpenFeature-backed feature flags provider for Beignet.
5
+ */
6
+ import { createProvider, createProviderInstrumentation, } from "@beignet/core/providers";
7
+ import { OpenFeature, } from "@openfeature/server-sdk";
8
+ function errorMessage(error) {
9
+ return error instanceof Error ? error.message : String(error);
10
+ }
11
+ function toOpenFeatureContext(context) {
12
+ if (!context)
13
+ return undefined;
14
+ return omitUndefined({
15
+ ...context.attributes,
16
+ targetingKey: context.targetingKey,
17
+ subjectType: context.subject?.type,
18
+ subjectId: context.subject?.id,
19
+ tenantId: context.tenant?.id,
20
+ tenantSlug: context.tenant?.slug,
21
+ requestId: context.requestId,
22
+ traceId: context.traceId,
23
+ spanId: context.spanId,
24
+ parentSpanId: context.parentSpanId,
25
+ traceparent: context.traceparent,
26
+ });
27
+ }
28
+ function sanitizedContext(context) {
29
+ if (!context)
30
+ return undefined;
31
+ const privateKeys = new Set(context.privateAttributes ?? []);
32
+ const attributes = Object.fromEntries(Object.entries(context.attributes ?? {}).map(([key, value]) => [
33
+ key,
34
+ privateKeys.has(key) ? "[redacted]" : value,
35
+ ]));
36
+ return {
37
+ ...context,
38
+ attributes,
39
+ };
40
+ }
41
+ function normalizeReason(reason) {
42
+ return (reason ?? "unknown").toLowerCase();
43
+ }
44
+ function omitUndefined(value) {
45
+ return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
46
+ }
47
+ function normalizeDetails(flag, defaultValue, details) {
48
+ const error = details.errorCode || details.errorMessage
49
+ ? {
50
+ code: details.errorCode,
51
+ message: details.errorMessage ?? details.errorCode ?? "Flag error",
52
+ }
53
+ : undefined;
54
+ const reason = normalizeReason(details.reason);
55
+ return {
56
+ key: flag.key,
57
+ kind: flag.kind,
58
+ value: details.value,
59
+ defaultValue,
60
+ reason,
61
+ defaulted: reason === "default" || reason === "error",
62
+ variant: details.variant,
63
+ metadata: details.flagMetadata,
64
+ error,
65
+ };
66
+ }
67
+ function defaultDetails(flag, defaultValue, error) {
68
+ return {
69
+ key: flag.key,
70
+ kind: flag.kind,
71
+ value: defaultValue,
72
+ defaultValue,
73
+ reason: "error",
74
+ defaulted: true,
75
+ error: { message: errorMessage(error) },
76
+ };
77
+ }
78
+ /**
79
+ * Create a Beignet FlagsPort backed by an OpenFeature client.
80
+ */
81
+ export function createOpenFeatureFlags(options) {
82
+ const instrumentation = createProviderInstrumentation(options.instrumentation, {
83
+ providerName: "flags-openfeature",
84
+ watcher: "flags",
85
+ });
86
+ const client = options.client;
87
+ const flags = {
88
+ async evaluate(flag, evaluationOptions) {
89
+ const details = await flags.details(flag, evaluationOptions);
90
+ return details.value;
91
+ },
92
+ async details(flag, evaluationOptions) {
93
+ const startedAt = Date.now();
94
+ const defaultValue = evaluationOptions?.defaultValue ?? flag.defaultValue;
95
+ const context = toOpenFeatureContext(evaluationOptions?.context);
96
+ try {
97
+ const details = await evaluateOpenFeatureDetails(client, flag, defaultValue, context);
98
+ const normalized = normalizeDetails(flag, defaultValue, details);
99
+ instrumentation.custom({
100
+ name: "flags.evaluate",
101
+ label: "Flag evaluated",
102
+ summary: `${flag.key} -> ${String(normalized.value)}`,
103
+ requestId: evaluationOptions?.context?.requestId,
104
+ traceId: evaluationOptions?.context?.traceId,
105
+ details: {
106
+ flagKey: flag.key,
107
+ kind: flag.kind,
108
+ reason: normalized.reason,
109
+ defaulted: normalized.defaulted,
110
+ variant: normalized.variant,
111
+ durationMs: Date.now() - startedAt,
112
+ context: sanitizedContext(evaluationOptions?.context),
113
+ },
114
+ });
115
+ if (evaluationOptions?.expose) {
116
+ await flags.recordExposure(flag, {
117
+ context: evaluationOptions.context,
118
+ value: normalized.value,
119
+ variant: normalized.variant,
120
+ metadata: normalized.metadata,
121
+ });
122
+ }
123
+ return normalized;
124
+ }
125
+ catch (error) {
126
+ const normalized = defaultDetails(flag, defaultValue, error);
127
+ instrumentation.custom({
128
+ name: "flags.evaluate.error",
129
+ label: "Flag evaluation failed",
130
+ summary: `${flag.key} -> default`,
131
+ requestId: evaluationOptions?.context?.requestId,
132
+ traceId: evaluationOptions?.context?.traceId,
133
+ details: {
134
+ flagKey: flag.key,
135
+ kind: flag.kind,
136
+ error: normalized.error,
137
+ durationMs: Date.now() - startedAt,
138
+ context: sanitizedContext(evaluationOptions?.context),
139
+ },
140
+ });
141
+ return normalized;
142
+ }
143
+ },
144
+ async recordExposure(flag, exposureOptions) {
145
+ const context = toOpenFeatureContext(exposureOptions?.context);
146
+ client.track("flag.exposure", context, omitUndefined({
147
+ ...exposureOptions?.metadata,
148
+ flagKey: flag.key,
149
+ variant: exposureOptions?.variant,
150
+ evaluatedValue: exposureOptions?.value,
151
+ }));
152
+ instrumentation.custom({
153
+ name: "flags.exposure",
154
+ label: "Flag exposure",
155
+ summary: flag.key,
156
+ requestId: exposureOptions?.context?.requestId,
157
+ traceId: exposureOptions?.context?.traceId,
158
+ details: {
159
+ flagKey: flag.key,
160
+ variant: exposureOptions?.variant,
161
+ context: sanitizedContext(exposureOptions?.context),
162
+ },
163
+ });
164
+ },
165
+ async track(event, trackOptions) {
166
+ client.track(event, toOpenFeatureContext(trackOptions?.context), omitUndefined({
167
+ ...trackOptions?.metadata,
168
+ value: trackOptions?.value,
169
+ }));
170
+ instrumentation.custom({
171
+ name: "flags.track",
172
+ label: "Flag tracking event",
173
+ summary: event,
174
+ requestId: trackOptions?.context?.requestId,
175
+ traceId: trackOptions?.context?.traceId,
176
+ details: {
177
+ event,
178
+ context: sanitizedContext(trackOptions?.context),
179
+ },
180
+ });
181
+ },
182
+ };
183
+ return flags;
184
+ }
185
+ async function evaluateOpenFeatureDetails(client, flag, defaultValue, context) {
186
+ switch (flag.kind) {
187
+ case "boolean":
188
+ return client.getBooleanDetails(flag.key, defaultValue, context);
189
+ case "string":
190
+ return client.getStringDetails(flag.key, defaultValue, context);
191
+ case "number":
192
+ return client.getNumberDetails(flag.key, defaultValue, context);
193
+ case "object":
194
+ return client.getObjectDetails(flag.key, defaultValue, context);
195
+ }
196
+ throw new Error(`Unsupported flag kind: ${String(flag.kind)}`);
197
+ }
198
+ function getClient(options) {
199
+ if (options.client)
200
+ return options.client;
201
+ if (options.domain && options.version) {
202
+ return OpenFeature.getClient(options.domain, options.version);
203
+ }
204
+ if (options.domain) {
205
+ return OpenFeature.getClient(options.domain);
206
+ }
207
+ return OpenFeature.getClient();
208
+ }
209
+ async function installProvider(options) {
210
+ if (!options.provider)
211
+ return;
212
+ if (options.waitForReady ?? true) {
213
+ if (options.domain) {
214
+ await OpenFeature.setProviderAndWait(options.domain, options.provider);
215
+ return;
216
+ }
217
+ await OpenFeature.setProviderAndWait(options.provider);
218
+ return;
219
+ }
220
+ if (options.domain) {
221
+ OpenFeature.setProvider(options.domain, options.provider);
222
+ return;
223
+ }
224
+ OpenFeature.setProvider(options.provider);
225
+ }
226
+ /**
227
+ * Create a Beignet provider that contributes `ports.flags`.
228
+ */
229
+ export function createOpenFeatureFlagsProvider(options = {}) {
230
+ return createProvider({
231
+ name: "flags-openfeature",
232
+ metadata: {
233
+ packageName: "@beignet/provider-flags-openfeature",
234
+ ports: ["flags", "openFeature"],
235
+ watchers: ["flags"],
236
+ },
237
+ async setup({ ports }) {
238
+ await installProvider(options);
239
+ const client = getClient(options);
240
+ return {
241
+ ports: {
242
+ flags: createOpenFeatureFlags({
243
+ client,
244
+ instrumentation: ports,
245
+ }),
246
+ openFeature: {
247
+ client,
248
+ api: OpenFeature,
249
+ },
250
+ },
251
+ async stop() {
252
+ if (options.clearOnStop) {
253
+ await OpenFeature.clearProviders();
254
+ }
255
+ },
256
+ };
257
+ },
258
+ });
259
+ }
260
+ /**
261
+ * Ready-to-register OpenFeature flags provider using the default OpenFeature
262
+ * singleton client.
263
+ */
264
+ export const openFeatureFlagsProvider = createOpenFeatureFlagsProvider();
265
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,93 @@
1
+ {
2
+ "name": "@beignet/provider-flags-openfeature",
3
+ "version": "0.0.14",
4
+ "type": "module",
5
+ "description": "OpenFeature feature flag provider for Beignet",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "src",
17
+ "!src/**/*.test.ts",
18
+ "!src/**/*.test.tsx",
19
+ "!src/**/*.test-d.ts",
20
+ "README.md",
21
+ "CHANGELOG.md"
22
+ ],
23
+ "scripts": {
24
+ "build": "tsc",
25
+ "dev": "tsc --watch",
26
+ "clean": "rm -rf dist coverage .turbo",
27
+ "test": "bun test",
28
+ "test:coverage": "bun test --coverage",
29
+ "lint": "biome check ."
30
+ },
31
+ "keywords": [
32
+ "beignet",
33
+ "openfeature",
34
+ "feature-flags",
35
+ "provider",
36
+ "ports"
37
+ ],
38
+ "license": "MIT",
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "git+https://github.com/taylorbryant/beignet.git",
42
+ "directory": "packages/provider-flags-openfeature"
43
+ },
44
+ "author": "Taylor Bryant",
45
+ "homepage": "https://github.com/taylorbryant/beignet#readme",
46
+ "bugs": "https://github.com/taylorbryant/beignet/issues",
47
+ "sideEffects": false,
48
+ "publishConfig": {
49
+ "access": "public"
50
+ },
51
+ "engines": {
52
+ "node": ">=18.0.0"
53
+ },
54
+ "peerDependencies": {
55
+ "@beignet/core": ">=0.0.3 <1.0.0",
56
+ "@openfeature/server-sdk": "^1.22.0"
57
+ },
58
+ "devDependencies": {
59
+ "@beignet/core": "*",
60
+ "@beignet/devtools": "*",
61
+ "@openfeature/server-sdk": "^1.22.0",
62
+ "@types/bun": "^1.3.13",
63
+ "@types/node": "^20.10.0",
64
+ "typescript": "^5.3.0"
65
+ },
66
+ "beignet": {
67
+ "provider": {
68
+ "displayName": "OpenFeature flags provider",
69
+ "ports": [
70
+ "flags",
71
+ "openFeature"
72
+ ],
73
+ "appPorts": [
74
+ {
75
+ "name": "flags",
76
+ "type": "FlagsPort"
77
+ }
78
+ ],
79
+ "env": [],
80
+ "requiredEnv": [],
81
+ "watchers": [
82
+ "flags"
83
+ ],
84
+ "registration": {
85
+ "required": true,
86
+ "tokens": [
87
+ "openFeatureFlagsProvider",
88
+ "createOpenFeatureFlagsProvider"
89
+ ]
90
+ }
91
+ }
92
+ }
93
+ }
package/src/index.ts ADDED
@@ -0,0 +1,438 @@
1
+ /**
2
+ * @beignet/provider-flags-openfeature
3
+ *
4
+ * OpenFeature-backed feature flags provider for Beignet.
5
+ */
6
+
7
+ import type {
8
+ FlagDef,
9
+ FlagEvaluationContext,
10
+ FlagEvaluationDetails,
11
+ FlagEvaluationOptions,
12
+ FlagExposureOptions,
13
+ FlagsPort,
14
+ FlagTrackOptions,
15
+ FlagValue,
16
+ } from "@beignet/core/flags";
17
+ import {
18
+ createProvider,
19
+ createProviderInstrumentation,
20
+ type ProviderInstrumentationTarget,
21
+ } from "@beignet/core/providers";
22
+ import {
23
+ type Client,
24
+ type EvaluationContext,
25
+ type EvaluationDetails,
26
+ type JsonValue,
27
+ OpenFeature,
28
+ type Provider,
29
+ type TrackingEventDetails,
30
+ } from "@openfeature/server-sdk";
31
+
32
+ /**
33
+ * Ports contributed by the OpenFeature flags provider.
34
+ */
35
+ export interface OpenFeatureFlagsProviderPorts {
36
+ /**
37
+ * Beignet feature flag port backed by OpenFeature.
38
+ */
39
+ flags: FlagsPort;
40
+ /**
41
+ * Escape hatch for advanced OpenFeature usage.
42
+ */
43
+ openFeature: OpenFeatureEscapeHatch;
44
+ }
45
+
46
+ /**
47
+ * Escape hatch for apps that need the raw OpenFeature client/API.
48
+ */
49
+ export interface OpenFeatureEscapeHatch {
50
+ client: Client;
51
+ api: typeof OpenFeature;
52
+ }
53
+
54
+ /**
55
+ * Options for the direct OpenFeature flags port factory.
56
+ */
57
+ export interface CreateOpenFeatureFlagsOptions {
58
+ client: Client;
59
+ instrumentation?: ProviderInstrumentationTarget;
60
+ }
61
+
62
+ /**
63
+ * Options for the OpenFeature provider package.
64
+ */
65
+ export interface CreateOpenFeatureFlagsProviderOptions {
66
+ /**
67
+ * Existing OpenFeature client. When supplied, setup does not register a
68
+ * provider with the singleton OpenFeature API.
69
+ */
70
+ client?: Client;
71
+ /**
72
+ * OpenFeature provider to install during Beignet server setup.
73
+ */
74
+ provider?: Provider;
75
+ /**
76
+ * Optional OpenFeature domain for provider and client binding.
77
+ */
78
+ domain?: string;
79
+ /**
80
+ * Optional client version used when creating a domain-scoped client.
81
+ */
82
+ version?: string;
83
+ /**
84
+ * Await provider initialization during setup.
85
+ *
86
+ * @default true
87
+ */
88
+ waitForReady?: boolean;
89
+ /**
90
+ * Clear OpenFeature providers when the Beignet server stops.
91
+ *
92
+ * @default false
93
+ */
94
+ clearOnStop?: boolean;
95
+ }
96
+
97
+ type OpenFeatureDetails = EvaluationDetails<
98
+ boolean | string | number | JsonValue
99
+ >;
100
+
101
+ function errorMessage(error: unknown): string {
102
+ return error instanceof Error ? error.message : String(error);
103
+ }
104
+
105
+ function toOpenFeatureContext(
106
+ context: FlagEvaluationContext | undefined,
107
+ ): EvaluationContext | undefined {
108
+ if (!context) return undefined;
109
+
110
+ return omitUndefined({
111
+ ...context.attributes,
112
+ targetingKey: context.targetingKey,
113
+ subjectType: context.subject?.type,
114
+ subjectId: context.subject?.id,
115
+ tenantId: context.tenant?.id,
116
+ tenantSlug: context.tenant?.slug,
117
+ requestId: context.requestId,
118
+ traceId: context.traceId,
119
+ spanId: context.spanId,
120
+ parentSpanId: context.parentSpanId,
121
+ traceparent: context.traceparent,
122
+ }) as EvaluationContext;
123
+ }
124
+
125
+ function sanitizedContext(context: FlagEvaluationContext | undefined) {
126
+ if (!context) return undefined;
127
+ const privateKeys = new Set(context.privateAttributes ?? []);
128
+ const attributes = Object.fromEntries(
129
+ Object.entries(context.attributes ?? {}).map(([key, value]) => [
130
+ key,
131
+ privateKeys.has(key) ? "[redacted]" : value,
132
+ ]),
133
+ );
134
+
135
+ return {
136
+ ...context,
137
+ attributes,
138
+ };
139
+ }
140
+
141
+ function normalizeReason(reason: string | undefined) {
142
+ return (reason ?? "unknown").toLowerCase();
143
+ }
144
+
145
+ function omitUndefined(
146
+ value: Record<string, unknown>,
147
+ ): Record<string, unknown> {
148
+ return Object.fromEntries(
149
+ Object.entries(value).filter(([, entry]) => entry !== undefined),
150
+ );
151
+ }
152
+
153
+ function normalizeDetails<TValue extends FlagValue>(
154
+ flag: FlagDef<TValue>,
155
+ defaultValue: TValue,
156
+ details: OpenFeatureDetails,
157
+ ): FlagEvaluationDetails<TValue> {
158
+ const error =
159
+ details.errorCode || details.errorMessage
160
+ ? {
161
+ code: details.errorCode,
162
+ message: details.errorMessage ?? details.errorCode ?? "Flag error",
163
+ }
164
+ : undefined;
165
+ const reason = normalizeReason(details.reason);
166
+
167
+ return {
168
+ key: flag.key,
169
+ kind: flag.kind,
170
+ value: details.value as TValue,
171
+ defaultValue,
172
+ reason,
173
+ defaulted: reason === "default" || reason === "error",
174
+ variant: details.variant,
175
+ metadata: details.flagMetadata,
176
+ error,
177
+ };
178
+ }
179
+
180
+ function defaultDetails<TValue extends FlagValue>(
181
+ flag: FlagDef<TValue>,
182
+ defaultValue: TValue,
183
+ error: unknown,
184
+ ): FlagEvaluationDetails<TValue> {
185
+ return {
186
+ key: flag.key,
187
+ kind: flag.kind,
188
+ value: defaultValue,
189
+ defaultValue,
190
+ reason: "error",
191
+ defaulted: true,
192
+ error: { message: errorMessage(error) },
193
+ };
194
+ }
195
+
196
+ /**
197
+ * Create a Beignet FlagsPort backed by an OpenFeature client.
198
+ */
199
+ export function createOpenFeatureFlags(
200
+ options: CreateOpenFeatureFlagsOptions,
201
+ ): FlagsPort {
202
+ const instrumentation = createProviderInstrumentation(
203
+ options.instrumentation,
204
+ {
205
+ providerName: "flags-openfeature",
206
+ watcher: "flags",
207
+ },
208
+ );
209
+ const client = options.client;
210
+
211
+ const flags: FlagsPort = {
212
+ async evaluate(flag, evaluationOptions) {
213
+ const details = await flags.details(flag, evaluationOptions);
214
+ return details.value;
215
+ },
216
+ async details(flag, evaluationOptions) {
217
+ const startedAt = Date.now();
218
+ const defaultValue = evaluationOptions?.defaultValue ?? flag.defaultValue;
219
+ const context = toOpenFeatureContext(evaluationOptions?.context);
220
+
221
+ try {
222
+ const details = await evaluateOpenFeatureDetails(
223
+ client,
224
+ flag,
225
+ defaultValue,
226
+ context,
227
+ );
228
+ const normalized = normalizeDetails(flag, defaultValue, details);
229
+
230
+ instrumentation.custom({
231
+ name: "flags.evaluate",
232
+ label: "Flag evaluated",
233
+ summary: `${flag.key} -> ${String(normalized.value)}`,
234
+ requestId: evaluationOptions?.context?.requestId,
235
+ traceId: evaluationOptions?.context?.traceId,
236
+ details: {
237
+ flagKey: flag.key,
238
+ kind: flag.kind,
239
+ reason: normalized.reason,
240
+ defaulted: normalized.defaulted,
241
+ variant: normalized.variant,
242
+ durationMs: Date.now() - startedAt,
243
+ context: sanitizedContext(evaluationOptions?.context),
244
+ },
245
+ });
246
+
247
+ if (evaluationOptions?.expose) {
248
+ await flags.recordExposure(flag, {
249
+ context: evaluationOptions.context,
250
+ value: normalized.value,
251
+ variant: normalized.variant,
252
+ metadata: normalized.metadata,
253
+ });
254
+ }
255
+
256
+ return normalized;
257
+ } catch (error) {
258
+ const normalized = defaultDetails(flag, defaultValue, error);
259
+ instrumentation.custom({
260
+ name: "flags.evaluate.error",
261
+ label: "Flag evaluation failed",
262
+ summary: `${flag.key} -> default`,
263
+ requestId: evaluationOptions?.context?.requestId,
264
+ traceId: evaluationOptions?.context?.traceId,
265
+ details: {
266
+ flagKey: flag.key,
267
+ kind: flag.kind,
268
+ error: normalized.error,
269
+ durationMs: Date.now() - startedAt,
270
+ context: sanitizedContext(evaluationOptions?.context),
271
+ },
272
+ });
273
+ return normalized;
274
+ }
275
+ },
276
+ async recordExposure(flag, exposureOptions) {
277
+ const context = toOpenFeatureContext(exposureOptions?.context);
278
+ client.track(
279
+ "flag.exposure",
280
+ context,
281
+ omitUndefined({
282
+ ...exposureOptions?.metadata,
283
+ flagKey: flag.key,
284
+ variant: exposureOptions?.variant,
285
+ evaluatedValue: exposureOptions?.value as JsonValue | undefined,
286
+ }) as TrackingEventDetails,
287
+ );
288
+ instrumentation.custom({
289
+ name: "flags.exposure",
290
+ label: "Flag exposure",
291
+ summary: flag.key,
292
+ requestId: exposureOptions?.context?.requestId,
293
+ traceId: exposureOptions?.context?.traceId,
294
+ details: {
295
+ flagKey: flag.key,
296
+ variant: exposureOptions?.variant,
297
+ context: sanitizedContext(exposureOptions?.context),
298
+ },
299
+ });
300
+ },
301
+ async track(event, trackOptions) {
302
+ client.track(
303
+ event,
304
+ toOpenFeatureContext(trackOptions?.context),
305
+ omitUndefined({
306
+ ...trackOptions?.metadata,
307
+ value: trackOptions?.value,
308
+ }) as TrackingEventDetails,
309
+ );
310
+ instrumentation.custom({
311
+ name: "flags.track",
312
+ label: "Flag tracking event",
313
+ summary: event,
314
+ requestId: trackOptions?.context?.requestId,
315
+ traceId: trackOptions?.context?.traceId,
316
+ details: {
317
+ event,
318
+ context: sanitizedContext(trackOptions?.context),
319
+ },
320
+ });
321
+ },
322
+ };
323
+
324
+ return flags;
325
+ }
326
+
327
+ async function evaluateOpenFeatureDetails<TValue extends FlagValue>(
328
+ client: Client,
329
+ flag: FlagDef<TValue>,
330
+ defaultValue: TValue,
331
+ context: EvaluationContext | undefined,
332
+ ): Promise<OpenFeatureDetails> {
333
+ switch (flag.kind) {
334
+ case "boolean":
335
+ return client.getBooleanDetails(
336
+ flag.key,
337
+ defaultValue as boolean,
338
+ context,
339
+ );
340
+ case "string":
341
+ return client.getStringDetails(flag.key, defaultValue as string, context);
342
+ case "number":
343
+ return client.getNumberDetails(flag.key, defaultValue as number, context);
344
+ case "object":
345
+ return client.getObjectDetails(
346
+ flag.key,
347
+ defaultValue as JsonValue,
348
+ context,
349
+ );
350
+ }
351
+
352
+ throw new Error(`Unsupported flag kind: ${String(flag.kind)}`);
353
+ }
354
+
355
+ function getClient(options: CreateOpenFeatureFlagsProviderOptions): Client {
356
+ if (options.client) return options.client;
357
+ if (options.domain && options.version) {
358
+ return OpenFeature.getClient(options.domain, options.version);
359
+ }
360
+ if (options.domain) {
361
+ return OpenFeature.getClient(options.domain);
362
+ }
363
+ return OpenFeature.getClient();
364
+ }
365
+
366
+ async function installProvider(
367
+ options: CreateOpenFeatureFlagsProviderOptions,
368
+ ): Promise<void> {
369
+ if (!options.provider) return;
370
+
371
+ if (options.waitForReady ?? true) {
372
+ if (options.domain) {
373
+ await OpenFeature.setProviderAndWait(options.domain, options.provider);
374
+ return;
375
+ }
376
+ await OpenFeature.setProviderAndWait(options.provider);
377
+ return;
378
+ }
379
+
380
+ if (options.domain) {
381
+ OpenFeature.setProvider(options.domain, options.provider);
382
+ return;
383
+ }
384
+ OpenFeature.setProvider(options.provider);
385
+ }
386
+
387
+ /**
388
+ * Create a Beignet provider that contributes `ports.flags`.
389
+ */
390
+ export function createOpenFeatureFlagsProvider(
391
+ options: CreateOpenFeatureFlagsProviderOptions = {},
392
+ ) {
393
+ return createProvider({
394
+ name: "flags-openfeature",
395
+ metadata: {
396
+ packageName: "@beignet/provider-flags-openfeature",
397
+ ports: ["flags", "openFeature"],
398
+ watchers: ["flags"],
399
+ },
400
+ async setup({ ports }) {
401
+ await installProvider(options);
402
+ const client = getClient(options);
403
+
404
+ return {
405
+ ports: {
406
+ flags: createOpenFeatureFlags({
407
+ client,
408
+ instrumentation: ports,
409
+ }),
410
+ openFeature: {
411
+ client,
412
+ api: OpenFeature,
413
+ },
414
+ } satisfies OpenFeatureFlagsProviderPorts,
415
+ async stop() {
416
+ if (options.clearOnStop) {
417
+ await OpenFeature.clearProviders();
418
+ }
419
+ },
420
+ };
421
+ },
422
+ });
423
+ }
424
+
425
+ /**
426
+ * Ready-to-register OpenFeature flags provider using the default OpenFeature
427
+ * singleton client.
428
+ */
429
+ export const openFeatureFlagsProvider = createOpenFeatureFlagsProvider();
430
+
431
+ export type {
432
+ FlagEvaluationContext,
433
+ FlagEvaluationDetails,
434
+ FlagEvaluationOptions,
435
+ FlagExposureOptions,
436
+ FlagsPort,
437
+ FlagTrackOptions,
438
+ };