@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 +14 -0
- package/LICENSE +21 -0
- package/README.md +68 -0
- package/dist/index.d.ts +96 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +265 -0
- package/dist/index.js.map +1 -0
- package/package.json +93 -0
- package/src/index.ts +438 -0
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.
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
};
|