@quonfig/openfeature-web 0.0.1

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/README.md ADDED
@@ -0,0 +1,119 @@
1
+ # @quonfig/openfeature-web
2
+
3
+ OpenFeature provider for [Quonfig](https://quonfig.com) — Web/Browser.
4
+
5
+ Works with both vanilla JS (`@openfeature/web-sdk`) and React (`@openfeature/react-sdk`).
6
+ The React SDK re-exports the web SDK and adds hooks (`useFlag`, `useBooleanFlagValue`, etc.) —
7
+ any web provider works with React hooks automatically.
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ # Vanilla web
13
+ npm install @quonfig/openfeature-web @quonfig/javascript @openfeature/web-sdk
14
+
15
+ # React
16
+ npm install @quonfig/openfeature-web @quonfig/javascript @openfeature/react-sdk
17
+ ```
18
+
19
+ ## Quick start
20
+
21
+ ### Vanilla JS
22
+
23
+ ```typescript
24
+ import { OpenFeature } from "@openfeature/web-sdk";
25
+ import { QuonfigWebProvider } from "@quonfig/openfeature-web";
26
+
27
+ const provider = new QuonfigWebProvider({
28
+ sdkKey: "qf_sk_...",
29
+ });
30
+
31
+ await OpenFeature.setContext({
32
+ targetingKey: "user-123",
33
+ "user.email": "alice@example.com",
34
+ "org.tier": "enterprise",
35
+ });
36
+
37
+ await OpenFeature.setProviderAndWait(provider);
38
+
39
+ const client = OpenFeature.getClient();
40
+ const isEnabled = client.getBooleanValue("my-flag", false);
41
+ ```
42
+
43
+ ### React
44
+
45
+ ```typescript
46
+ import { OpenFeatureProvider, useBooleanFlagValue } from "@openfeature/react-sdk";
47
+ import { OpenFeature } from "@openfeature/web-sdk";
48
+ import { QuonfigWebProvider } from "@quonfig/openfeature-web";
49
+
50
+ const provider = new QuonfigWebProvider({ sdkKey: "qf_sk_..." });
51
+ await OpenFeature.setProviderAndWait(provider);
52
+
53
+ function MyComponent() {
54
+ const enabled = useBooleanFlagValue("my-flag", false);
55
+ return <div>{enabled ? "Feature on" : "Feature off"}</div>;
56
+ }
57
+
58
+ function App() {
59
+ return (
60
+ <OpenFeatureProvider>
61
+ <MyComponent />
62
+ </OpenFeatureProvider>
63
+ );
64
+ }
65
+ ```
66
+
67
+ ## Configuration
68
+
69
+ ```typescript
70
+ const provider = new QuonfigWebProvider({
71
+ sdkKey: "qf_sk_...", // required
72
+ targetingKeyMapping: "user.id", // default; maps OpenFeature targetingKey
73
+ apiUrl: "https://custom.api.com", // optional — override API base URL
74
+ timeout: 5000, // optional — request timeout in ms
75
+ });
76
+ ```
77
+
78
+ ## Context mapping
79
+
80
+ OpenFeature uses a flat context; Quonfig uses a namespace-nested context.
81
+ The provider maps between them using dot-notation:
82
+
83
+ | OpenFeature key | Quonfig context |
84
+ |-------------------------|-----------------------------|
85
+ | `targetingKey: "u-123"` | `{ user: { id: "u-123" } }` |
86
+ | `"user.email": "a@b.c"` | `{ user: { email: "a@b.c" } }` |
87
+ | `"org.tier": "pro"` | `{ org: { tier: "pro" } }` |
88
+ | `"country": "US"` | `{ "": { country: "US" } }` |
89
+
90
+ Keys without a dot go into the default (empty-string) namespace.
91
+
92
+ To use a different property for `targetingKey`:
93
+
94
+ ```typescript
95
+ new QuonfigWebProvider({ sdkKey: "...", targetingKeyMapping: "account.id" });
96
+ ```
97
+
98
+ ## What you lose vs. the native SDK
99
+
100
+ The OpenFeature interface covers boolean, string, number, and object types.
101
+ Some Quonfig-native features require `provider.getClient()` (the escape hatch):
102
+
103
+ 1. **Log levels** (`shouldLog`, `logger`) — native SDK only
104
+ 2. **`string_list` configs** — access via `getObjectValue` and cast to `string[]`
105
+ 3. **`duration` configs** — `getStringValue` returns an ISO 8601 string; parse client-side
106
+ 4. **`bytes` configs** — not accessible via OpenFeature
107
+ 5. **`keys()` and `raw()`** — native SDK only
108
+ 6. **Context keys** must use dot-notation (`"user.email"`), not nested objects
109
+ 7. **`targetingKey`** maps to `user.id` by default — configure `targetingKeyMapping` if different
110
+
111
+ ```typescript
112
+ // Escape hatch for Quonfig-native features
113
+ const native = provider.getClient();
114
+ native.shouldLog({ loggerName: "auth", desiredLevel: "DEBUG", defaultLevel: "WARN" });
115
+ ```
116
+
117
+ ## License
118
+
119
+ MIT
@@ -0,0 +1,60 @@
1
+ import { Provider, OpenFeatureEventEmitter, EvaluationContext, ResolutionDetails, JsonValue, ErrorCode } from '@openfeature/web-sdk';
2
+ import { Quonfig, Contexts } from '@quonfig/javascript';
3
+
4
+ interface QuonfigWebProviderOptions {
5
+ sdkKey: string;
6
+ /** Which Quonfig context property the OpenFeature targetingKey maps to. Default: "user.id" */
7
+ targetingKeyMapping?: string;
8
+ /** Override the Quonfig API base URL. */
9
+ apiUrl?: string;
10
+ /** Request timeout in ms. */
11
+ timeout?: number;
12
+ }
13
+ declare class QuonfigWebProvider implements Provider {
14
+ readonly metadata: {
15
+ readonly name: "quonfig-web";
16
+ };
17
+ readonly runsOn: "client";
18
+ hooks: never[];
19
+ readonly events: OpenFeatureEventEmitter;
20
+ private client;
21
+ private readonly targetingKeyMapping;
22
+ private readonly sdkKey;
23
+ private readonly apiUrl;
24
+ private readonly timeout;
25
+ constructor(options: QuonfigWebProviderOptions);
26
+ initialize(context?: EvaluationContext): Promise<void>;
27
+ onContextChanged(_oldCtx: EvaluationContext, newCtx: EvaluationContext): Promise<void>;
28
+ shutdown(): Promise<void>;
29
+ resolveBooleanEvaluation(flagKey: string, defaultValue: boolean, _context?: EvaluationContext): ResolutionDetails<boolean>;
30
+ resolveStringEvaluation(flagKey: string, defaultValue: string, _context?: EvaluationContext): ResolutionDetails<string>;
31
+ resolveNumberEvaluation(flagKey: string, defaultValue: number, _context?: EvaluationContext): ResolutionDetails<number>;
32
+ resolveObjectEvaluation<T extends JsonValue = JsonValue>(flagKey: string, defaultValue: T, _context?: EvaluationContext): ResolutionDetails<T>;
33
+ /** Escape hatch: access the underlying Quonfig client directly. */
34
+ getClient(): Quonfig;
35
+ private _resolve;
36
+ /**
37
+ * Coerce a raw ConfigValue to the expected OF type.
38
+ * Returns null if the type does not match (signals TYPE_MISMATCH).
39
+ */
40
+ private _coerce;
41
+ /** Convert a Quonfig Duration to an ISO 8601 duration string (e.g. "PT1H30M"). */
42
+ private _durationToISO;
43
+ }
44
+
45
+ /**
46
+ * Map an OpenFeature EvaluationContext to Quonfig's nested Contexts format.
47
+ *
48
+ * Rules:
49
+ * - `targetingKey` maps to the property specified by `targetingKeyMapping` (default: "user.id")
50
+ * - Keys with a dot are split on the FIRST dot: namespace = left side, key = right side
51
+ * - Keys without a dot go into the default ("") namespace
52
+ */
53
+ declare function mapContext(ofContext: EvaluationContext, targetingKeyMapping?: string): Contexts;
54
+
55
+ /**
56
+ * Map a native SDK error (or unknown throw) to an OpenFeature ErrorCode.
57
+ */
58
+ declare function toErrorCode(err: unknown): ErrorCode;
59
+
60
+ export { QuonfigWebProvider, type QuonfigWebProviderOptions, mapContext, toErrorCode };
@@ -0,0 +1,60 @@
1
+ import { Provider, OpenFeatureEventEmitter, EvaluationContext, ResolutionDetails, JsonValue, ErrorCode } from '@openfeature/web-sdk';
2
+ import { Quonfig, Contexts } from '@quonfig/javascript';
3
+
4
+ interface QuonfigWebProviderOptions {
5
+ sdkKey: string;
6
+ /** Which Quonfig context property the OpenFeature targetingKey maps to. Default: "user.id" */
7
+ targetingKeyMapping?: string;
8
+ /** Override the Quonfig API base URL. */
9
+ apiUrl?: string;
10
+ /** Request timeout in ms. */
11
+ timeout?: number;
12
+ }
13
+ declare class QuonfigWebProvider implements Provider {
14
+ readonly metadata: {
15
+ readonly name: "quonfig-web";
16
+ };
17
+ readonly runsOn: "client";
18
+ hooks: never[];
19
+ readonly events: OpenFeatureEventEmitter;
20
+ private client;
21
+ private readonly targetingKeyMapping;
22
+ private readonly sdkKey;
23
+ private readonly apiUrl;
24
+ private readonly timeout;
25
+ constructor(options: QuonfigWebProviderOptions);
26
+ initialize(context?: EvaluationContext): Promise<void>;
27
+ onContextChanged(_oldCtx: EvaluationContext, newCtx: EvaluationContext): Promise<void>;
28
+ shutdown(): Promise<void>;
29
+ resolveBooleanEvaluation(flagKey: string, defaultValue: boolean, _context?: EvaluationContext): ResolutionDetails<boolean>;
30
+ resolveStringEvaluation(flagKey: string, defaultValue: string, _context?: EvaluationContext): ResolutionDetails<string>;
31
+ resolveNumberEvaluation(flagKey: string, defaultValue: number, _context?: EvaluationContext): ResolutionDetails<number>;
32
+ resolveObjectEvaluation<T extends JsonValue = JsonValue>(flagKey: string, defaultValue: T, _context?: EvaluationContext): ResolutionDetails<T>;
33
+ /** Escape hatch: access the underlying Quonfig client directly. */
34
+ getClient(): Quonfig;
35
+ private _resolve;
36
+ /**
37
+ * Coerce a raw ConfigValue to the expected OF type.
38
+ * Returns null if the type does not match (signals TYPE_MISMATCH).
39
+ */
40
+ private _coerce;
41
+ /** Convert a Quonfig Duration to an ISO 8601 duration string (e.g. "PT1H30M"). */
42
+ private _durationToISO;
43
+ }
44
+
45
+ /**
46
+ * Map an OpenFeature EvaluationContext to Quonfig's nested Contexts format.
47
+ *
48
+ * Rules:
49
+ * - `targetingKey` maps to the property specified by `targetingKeyMapping` (default: "user.id")
50
+ * - Keys with a dot are split on the FIRST dot: namespace = left side, key = right side
51
+ * - Keys without a dot go into the default ("") namespace
52
+ */
53
+ declare function mapContext(ofContext: EvaluationContext, targetingKeyMapping?: string): Contexts;
54
+
55
+ /**
56
+ * Map a native SDK error (or unknown throw) to an OpenFeature ErrorCode.
57
+ */
58
+ declare function toErrorCode(err: unknown): ErrorCode;
59
+
60
+ export { QuonfigWebProvider, type QuonfigWebProviderOptions, mapContext, toErrorCode };
package/dist/index.js ADDED
@@ -0,0 +1,199 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ QuonfigWebProvider: () => QuonfigWebProvider,
24
+ mapContext: () => mapContext,
25
+ toErrorCode: () => toErrorCode
26
+ });
27
+ module.exports = __toCommonJS(index_exports);
28
+
29
+ // src/provider.ts
30
+ var import_web_sdk2 = require("@openfeature/web-sdk");
31
+ var import_javascript = require("@quonfig/javascript");
32
+
33
+ // src/context.ts
34
+ function mapContext(ofContext, targetingKeyMapping = "user.id") {
35
+ const result = {};
36
+ for (const [key, value] of Object.entries(ofContext)) {
37
+ if (value === void 0) continue;
38
+ if (key === "targetingKey") {
39
+ const dotIdx2 = targetingKeyMapping.indexOf(".");
40
+ const ns = dotIdx2 === -1 ? "" : targetingKeyMapping.slice(0, dotIdx2);
41
+ const prop = dotIdx2 === -1 ? targetingKeyMapping : targetingKeyMapping.slice(dotIdx2 + 1);
42
+ result[ns] ?? (result[ns] = {});
43
+ result[ns][prop] = value;
44
+ continue;
45
+ }
46
+ const dotIdx = key.indexOf(".");
47
+ if (dotIdx === -1) {
48
+ result[""] ?? (result[""] = {});
49
+ result[""][key] = value;
50
+ } else {
51
+ const ns = key.slice(0, dotIdx);
52
+ const prop = key.slice(dotIdx + 1);
53
+ result[ns] ?? (result[ns] = {});
54
+ result[ns][prop] = value;
55
+ }
56
+ }
57
+ return result;
58
+ }
59
+
60
+ // src/errors.ts
61
+ var import_web_sdk = require("@openfeature/web-sdk");
62
+ function toErrorCode(err) {
63
+ const msg = err instanceof Error ? err.message.toLowerCase() : String(err).toLowerCase();
64
+ if (msg.includes("flag not found") || msg.includes("not found")) {
65
+ return import_web_sdk.ErrorCode.FLAG_NOT_FOUND;
66
+ }
67
+ if (msg.includes("type mismatch") || msg.includes("type_mismatch")) {
68
+ return import_web_sdk.ErrorCode.TYPE_MISMATCH;
69
+ }
70
+ if (msg.includes("not initialized") || msg.includes("provider_not_ready") || msg.includes("call init()") || msg.includes("not ready")) {
71
+ return import_web_sdk.ErrorCode.PROVIDER_NOT_READY;
72
+ }
73
+ return import_web_sdk.ErrorCode.GENERAL;
74
+ }
75
+
76
+ // src/provider.ts
77
+ var QuonfigWebProvider = class {
78
+ constructor(options) {
79
+ this.metadata = { name: "quonfig-web" };
80
+ this.runsOn = "client";
81
+ this.hooks = [];
82
+ this.events = new import_web_sdk2.OpenFeatureEventEmitter();
83
+ this.sdkKey = options.sdkKey;
84
+ this.targetingKeyMapping = options.targetingKeyMapping ?? "user.id";
85
+ this.apiUrl = options.apiUrl;
86
+ this.timeout = options.timeout;
87
+ this.client = new import_javascript.Quonfig();
88
+ }
89
+ async initialize(context) {
90
+ const nativeCtx = context ? mapContext(context, this.targetingKeyMapping) : { "": {} };
91
+ await this.client.init({
92
+ sdkKey: this.sdkKey,
93
+ context: nativeCtx,
94
+ ...this.apiUrl !== void 0 && { apiUrl: this.apiUrl },
95
+ ...this.timeout !== void 0 && { timeout: this.timeout }
96
+ });
97
+ }
98
+ async onContextChanged(_oldCtx, newCtx) {
99
+ const nativeCtx = mapContext(newCtx, this.targetingKeyMapping);
100
+ await this.client.updateContext(nativeCtx);
101
+ }
102
+ async shutdown() {
103
+ this.client.close();
104
+ }
105
+ resolveBooleanEvaluation(flagKey, defaultValue, _context) {
106
+ return this._resolve(flagKey, defaultValue, "boolean");
107
+ }
108
+ resolveStringEvaluation(flagKey, defaultValue, _context) {
109
+ return this._resolve(flagKey, defaultValue, "string");
110
+ }
111
+ resolveNumberEvaluation(flagKey, defaultValue, _context) {
112
+ return this._resolve(flagKey, defaultValue, "number");
113
+ }
114
+ resolveObjectEvaluation(flagKey, defaultValue, _context) {
115
+ return this._resolve(flagKey, defaultValue, "object");
116
+ }
117
+ /** Escape hatch: access the underlying Quonfig client directly. */
118
+ getClient() {
119
+ return this.client;
120
+ }
121
+ // ---------------------------------------------------------------------------
122
+ // Private helpers
123
+ // ---------------------------------------------------------------------------
124
+ _resolve(flagKey, defaultValue, expectedType) {
125
+ try {
126
+ const raw = this.client.get(flagKey);
127
+ if (raw === void 0 || raw === null) {
128
+ return {
129
+ value: defaultValue,
130
+ reason: import_web_sdk2.StandardResolutionReasons.DEFAULT,
131
+ errorCode: import_web_sdk2.ErrorCode.FLAG_NOT_FOUND
132
+ };
133
+ }
134
+ const coerced = this._coerce(raw, expectedType, defaultValue);
135
+ if (coerced === null) {
136
+ return {
137
+ value: defaultValue,
138
+ reason: import_web_sdk2.StandardResolutionReasons.ERROR,
139
+ errorCode: import_web_sdk2.ErrorCode.TYPE_MISMATCH
140
+ };
141
+ }
142
+ return {
143
+ value: coerced,
144
+ reason: import_web_sdk2.StandardResolutionReasons.STATIC
145
+ };
146
+ } catch (err) {
147
+ return {
148
+ value: defaultValue,
149
+ reason: import_web_sdk2.StandardResolutionReasons.ERROR,
150
+ errorCode: toErrorCode(err)
151
+ };
152
+ }
153
+ }
154
+ /**
155
+ * Coerce a raw ConfigValue to the expected OF type.
156
+ * Returns null if the type does not match (signals TYPE_MISMATCH).
157
+ */
158
+ _coerce(raw, expectedType, defaultValue) {
159
+ switch (expectedType) {
160
+ case "boolean":
161
+ if (typeof raw === "boolean") return raw;
162
+ return null;
163
+ case "string":
164
+ if (typeof raw === "string") return raw;
165
+ if (raw !== null && typeof raw === "object" && "seconds" in raw && "ms" in raw) {
166
+ return this._durationToISO(raw);
167
+ }
168
+ return null;
169
+ case "number":
170
+ if (typeof raw === "number") return raw;
171
+ return null;
172
+ case "object":
173
+ if (Array.isArray(raw)) return raw;
174
+ if (raw !== null && typeof raw === "object") return raw;
175
+ return null;
176
+ default:
177
+ return null;
178
+ }
179
+ }
180
+ /** Convert a Quonfig Duration to an ISO 8601 duration string (e.g. "PT1H30M"). */
181
+ _durationToISO(duration) {
182
+ const totalSeconds = duration.seconds;
183
+ const hours = Math.floor(totalSeconds / 3600);
184
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
185
+ const secs = totalSeconds % 60;
186
+ let iso = "PT";
187
+ if (hours > 0) iso += `${hours}H`;
188
+ if (minutes > 0) iso += `${minutes}M`;
189
+ if (secs > 0 || iso === "PT") iso += `${secs}S`;
190
+ return iso;
191
+ }
192
+ };
193
+ // Annotate the CommonJS export names for ESM import in node:
194
+ 0 && (module.exports = {
195
+ QuonfigWebProvider,
196
+ mapContext,
197
+ toErrorCode
198
+ });
199
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/provider.ts","../src/context.ts","../src/errors.ts"],"sourcesContent":["export { QuonfigWebProvider } from \"./provider\";\nexport type { QuonfigWebProviderOptions } from \"./provider\";\nexport { mapContext } from \"./context\";\nexport { toErrorCode } from \"./errors\";\n","import {\n ErrorCode,\n OpenFeatureEventEmitter,\n Provider,\n ResolutionDetails,\n StandardResolutionReasons,\n} from \"@openfeature/web-sdk\";\nimport type { EvaluationContext, JsonValue } from \"@openfeature/web-sdk\";\nimport { Quonfig } from \"@quonfig/javascript\";\n\nimport { mapContext } from \"./context\";\nimport { toErrorCode } from \"./errors\";\n\nexport interface QuonfigWebProviderOptions {\n sdkKey: string;\n /** Which Quonfig context property the OpenFeature targetingKey maps to. Default: \"user.id\" */\n targetingKeyMapping?: string;\n /** Override the Quonfig API base URL. */\n apiUrl?: string;\n /** Request timeout in ms. */\n timeout?: number;\n}\n\nexport class QuonfigWebProvider implements Provider {\n readonly metadata = { name: \"quonfig-web\" } as const;\n readonly runsOn = \"client\" as const;\n hooks = [];\n readonly events = new OpenFeatureEventEmitter();\n\n private client: Quonfig;\n private readonly targetingKeyMapping: string;\n private readonly sdkKey: string;\n private readonly apiUrl: string | undefined;\n private readonly timeout: number | undefined;\n\n constructor(options: QuonfigWebProviderOptions) {\n this.sdkKey = options.sdkKey;\n this.targetingKeyMapping = options.targetingKeyMapping ?? \"user.id\";\n this.apiUrl = options.apiUrl;\n this.timeout = options.timeout;\n this.client = new Quonfig();\n }\n\n async initialize(context?: EvaluationContext): Promise<void> {\n const nativeCtx = context\n ? mapContext(context, this.targetingKeyMapping)\n : { \"\": {} };\n\n await this.client.init({\n sdkKey: this.sdkKey,\n context: nativeCtx,\n ...(this.apiUrl !== undefined && { apiUrl: this.apiUrl }),\n ...(this.timeout !== undefined && { timeout: this.timeout }),\n });\n }\n\n async onContextChanged(\n _oldCtx: EvaluationContext,\n newCtx: EvaluationContext\n ): Promise<void> {\n const nativeCtx = mapContext(newCtx, this.targetingKeyMapping);\n await this.client.updateContext(nativeCtx);\n }\n\n async shutdown(): Promise<void> {\n this.client.close();\n }\n\n resolveBooleanEvaluation(\n flagKey: string,\n defaultValue: boolean,\n _context?: EvaluationContext\n ): ResolutionDetails<boolean> {\n return this._resolve(flagKey, defaultValue, \"boolean\");\n }\n\n resolveStringEvaluation(\n flagKey: string,\n defaultValue: string,\n _context?: EvaluationContext\n ): ResolutionDetails<string> {\n return this._resolve(flagKey, defaultValue, \"string\");\n }\n\n resolveNumberEvaluation(\n flagKey: string,\n defaultValue: number,\n _context?: EvaluationContext\n ): ResolutionDetails<number> {\n return this._resolve(flagKey, defaultValue, \"number\");\n }\n\n resolveObjectEvaluation<T extends JsonValue = JsonValue>(\n flagKey: string,\n defaultValue: T,\n _context?: EvaluationContext\n ): ResolutionDetails<T> {\n return this._resolve(flagKey, defaultValue, \"object\");\n }\n\n /** Escape hatch: access the underlying Quonfig client directly. */\n getClient(): Quonfig {\n return this.client;\n }\n\n // ---------------------------------------------------------------------------\n // Private helpers\n // ---------------------------------------------------------------------------\n\n private _resolve<T>(\n flagKey: string,\n defaultValue: T,\n expectedType: \"boolean\" | \"string\" | \"number\" | \"object\"\n ): ResolutionDetails<T> {\n try {\n const raw = this.client.get(flagKey);\n\n if (raw === undefined || raw === null) {\n // Flag not found — return OF default\n return {\n value: defaultValue,\n reason: StandardResolutionReasons.DEFAULT,\n errorCode: ErrorCode.FLAG_NOT_FOUND,\n };\n }\n\n // Type coercion / validation\n const coerced = this._coerce<T>(raw, expectedType, defaultValue);\n if (coerced === null) {\n return {\n value: defaultValue,\n reason: StandardResolutionReasons.ERROR,\n errorCode: ErrorCode.TYPE_MISMATCH,\n };\n }\n\n return {\n value: coerced,\n reason: StandardResolutionReasons.STATIC,\n };\n } catch (err) {\n return {\n value: defaultValue,\n reason: StandardResolutionReasons.ERROR,\n errorCode: toErrorCode(err),\n };\n }\n }\n\n /**\n * Coerce a raw ConfigValue to the expected OF type.\n * Returns null if the type does not match (signals TYPE_MISMATCH).\n */\n private _coerce<T>(\n raw: unknown,\n expectedType: \"boolean\" | \"string\" | \"number\" | \"object\",\n defaultValue: T\n ): T | null {\n switch (expectedType) {\n case \"boolean\":\n if (typeof raw === \"boolean\") return raw as unknown as T;\n return null;\n\n case \"string\":\n if (typeof raw === \"string\") return raw as unknown as T;\n // Duration objects: return ISO 8601 string representation\n if (\n raw !== null &&\n typeof raw === \"object\" &&\n \"seconds\" in raw &&\n \"ms\" in raw\n ) {\n return this._durationToISO(raw as { seconds: number; ms: number }) as unknown as T;\n }\n return null;\n\n case \"number\":\n if (typeof raw === \"number\") return raw as unknown as T;\n return null;\n\n case \"object\":\n // Arrays (string_list) and plain objects both satisfy \"object\"\n if (Array.isArray(raw)) return raw as unknown as T;\n if (raw !== null && typeof raw === \"object\") return raw as unknown as T;\n return null;\n\n default:\n return null;\n }\n }\n\n /** Convert a Quonfig Duration to an ISO 8601 duration string (e.g. \"PT1H30M\"). */\n private _durationToISO(duration: { seconds: number; ms: number }): string {\n const totalSeconds = duration.seconds;\n const hours = Math.floor(totalSeconds / 3600);\n const minutes = Math.floor((totalSeconds % 3600) / 60);\n const secs = totalSeconds % 60;\n\n let iso = \"PT\";\n if (hours > 0) iso += `${hours}H`;\n if (minutes > 0) iso += `${minutes}M`;\n if (secs > 0 || iso === \"PT\") iso += `${secs}S`;\n return iso;\n }\n}\n","import type { EvaluationContext } from \"@openfeature/web-sdk\";\nimport type { Contexts } from \"@quonfig/javascript\";\n\n/**\n * Map an OpenFeature EvaluationContext to Quonfig's nested Contexts format.\n *\n * Rules:\n * - `targetingKey` maps to the property specified by `targetingKeyMapping` (default: \"user.id\")\n * - Keys with a dot are split on the FIRST dot: namespace = left side, key = right side\n * - Keys without a dot go into the default (\"\") namespace\n */\nexport function mapContext(\n ofContext: EvaluationContext,\n targetingKeyMapping = \"user.id\"\n): Contexts {\n const result: Record<string, Record<string, unknown>> = {};\n\n for (const [key, value] of Object.entries(ofContext)) {\n if (value === undefined) continue;\n\n if (key === \"targetingKey\") {\n const dotIdx = targetingKeyMapping.indexOf(\".\");\n const ns = dotIdx === -1 ? \"\" : targetingKeyMapping.slice(0, dotIdx);\n const prop =\n dotIdx === -1 ? targetingKeyMapping : targetingKeyMapping.slice(dotIdx + 1);\n result[ns] ??= {};\n result[ns][prop] = value;\n continue;\n }\n\n const dotIdx = key.indexOf(\".\");\n if (dotIdx === -1) {\n result[\"\"] ??= {};\n result[\"\"][key] = value;\n } else {\n const ns = key.slice(0, dotIdx);\n const prop = key.slice(dotIdx + 1);\n result[ns] ??= {};\n result[ns][prop] = value;\n }\n }\n\n return result as Contexts;\n}\n","import { ErrorCode } from \"@openfeature/web-sdk\";\n\n/**\n * Map a native SDK error (or unknown throw) to an OpenFeature ErrorCode.\n */\nexport function toErrorCode(err: unknown): ErrorCode {\n const msg =\n err instanceof Error ? err.message.toLowerCase() : String(err).toLowerCase();\n\n if (msg.includes(\"flag not found\") || msg.includes(\"not found\")) {\n return ErrorCode.FLAG_NOT_FOUND;\n }\n if (msg.includes(\"type mismatch\") || msg.includes(\"type_mismatch\")) {\n return ErrorCode.TYPE_MISMATCH;\n }\n if (\n msg.includes(\"not initialized\") ||\n msg.includes(\"provider_not_ready\") ||\n msg.includes(\"call init()\") ||\n msg.includes(\"not ready\")\n ) {\n return ErrorCode.PROVIDER_NOT_READY;\n }\n return ErrorCode.GENERAL;\n}\n"],"mappings":"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","names":["import_web_sdk","dotIdx"]}
package/dist/index.mjs ADDED
@@ -0,0 +1,174 @@
1
+ // src/provider.ts
2
+ import {
3
+ ErrorCode as ErrorCode2,
4
+ OpenFeatureEventEmitter,
5
+ StandardResolutionReasons
6
+ } from "@openfeature/web-sdk";
7
+ import { Quonfig } from "@quonfig/javascript";
8
+
9
+ // src/context.ts
10
+ function mapContext(ofContext, targetingKeyMapping = "user.id") {
11
+ const result = {};
12
+ for (const [key, value] of Object.entries(ofContext)) {
13
+ if (value === void 0) continue;
14
+ if (key === "targetingKey") {
15
+ const dotIdx2 = targetingKeyMapping.indexOf(".");
16
+ const ns = dotIdx2 === -1 ? "" : targetingKeyMapping.slice(0, dotIdx2);
17
+ const prop = dotIdx2 === -1 ? targetingKeyMapping : targetingKeyMapping.slice(dotIdx2 + 1);
18
+ result[ns] ?? (result[ns] = {});
19
+ result[ns][prop] = value;
20
+ continue;
21
+ }
22
+ const dotIdx = key.indexOf(".");
23
+ if (dotIdx === -1) {
24
+ result[""] ?? (result[""] = {});
25
+ result[""][key] = value;
26
+ } else {
27
+ const ns = key.slice(0, dotIdx);
28
+ const prop = key.slice(dotIdx + 1);
29
+ result[ns] ?? (result[ns] = {});
30
+ result[ns][prop] = value;
31
+ }
32
+ }
33
+ return result;
34
+ }
35
+
36
+ // src/errors.ts
37
+ import { ErrorCode } from "@openfeature/web-sdk";
38
+ function toErrorCode(err) {
39
+ const msg = err instanceof Error ? err.message.toLowerCase() : String(err).toLowerCase();
40
+ if (msg.includes("flag not found") || msg.includes("not found")) {
41
+ return ErrorCode.FLAG_NOT_FOUND;
42
+ }
43
+ if (msg.includes("type mismatch") || msg.includes("type_mismatch")) {
44
+ return ErrorCode.TYPE_MISMATCH;
45
+ }
46
+ if (msg.includes("not initialized") || msg.includes("provider_not_ready") || msg.includes("call init()") || msg.includes("not ready")) {
47
+ return ErrorCode.PROVIDER_NOT_READY;
48
+ }
49
+ return ErrorCode.GENERAL;
50
+ }
51
+
52
+ // src/provider.ts
53
+ var QuonfigWebProvider = class {
54
+ constructor(options) {
55
+ this.metadata = { name: "quonfig-web" };
56
+ this.runsOn = "client";
57
+ this.hooks = [];
58
+ this.events = new OpenFeatureEventEmitter();
59
+ this.sdkKey = options.sdkKey;
60
+ this.targetingKeyMapping = options.targetingKeyMapping ?? "user.id";
61
+ this.apiUrl = options.apiUrl;
62
+ this.timeout = options.timeout;
63
+ this.client = new Quonfig();
64
+ }
65
+ async initialize(context) {
66
+ const nativeCtx = context ? mapContext(context, this.targetingKeyMapping) : { "": {} };
67
+ await this.client.init({
68
+ sdkKey: this.sdkKey,
69
+ context: nativeCtx,
70
+ ...this.apiUrl !== void 0 && { apiUrl: this.apiUrl },
71
+ ...this.timeout !== void 0 && { timeout: this.timeout }
72
+ });
73
+ }
74
+ async onContextChanged(_oldCtx, newCtx) {
75
+ const nativeCtx = mapContext(newCtx, this.targetingKeyMapping);
76
+ await this.client.updateContext(nativeCtx);
77
+ }
78
+ async shutdown() {
79
+ this.client.close();
80
+ }
81
+ resolveBooleanEvaluation(flagKey, defaultValue, _context) {
82
+ return this._resolve(flagKey, defaultValue, "boolean");
83
+ }
84
+ resolveStringEvaluation(flagKey, defaultValue, _context) {
85
+ return this._resolve(flagKey, defaultValue, "string");
86
+ }
87
+ resolveNumberEvaluation(flagKey, defaultValue, _context) {
88
+ return this._resolve(flagKey, defaultValue, "number");
89
+ }
90
+ resolveObjectEvaluation(flagKey, defaultValue, _context) {
91
+ return this._resolve(flagKey, defaultValue, "object");
92
+ }
93
+ /** Escape hatch: access the underlying Quonfig client directly. */
94
+ getClient() {
95
+ return this.client;
96
+ }
97
+ // ---------------------------------------------------------------------------
98
+ // Private helpers
99
+ // ---------------------------------------------------------------------------
100
+ _resolve(flagKey, defaultValue, expectedType) {
101
+ try {
102
+ const raw = this.client.get(flagKey);
103
+ if (raw === void 0 || raw === null) {
104
+ return {
105
+ value: defaultValue,
106
+ reason: StandardResolutionReasons.DEFAULT,
107
+ errorCode: ErrorCode2.FLAG_NOT_FOUND
108
+ };
109
+ }
110
+ const coerced = this._coerce(raw, expectedType, defaultValue);
111
+ if (coerced === null) {
112
+ return {
113
+ value: defaultValue,
114
+ reason: StandardResolutionReasons.ERROR,
115
+ errorCode: ErrorCode2.TYPE_MISMATCH
116
+ };
117
+ }
118
+ return {
119
+ value: coerced,
120
+ reason: StandardResolutionReasons.STATIC
121
+ };
122
+ } catch (err) {
123
+ return {
124
+ value: defaultValue,
125
+ reason: StandardResolutionReasons.ERROR,
126
+ errorCode: toErrorCode(err)
127
+ };
128
+ }
129
+ }
130
+ /**
131
+ * Coerce a raw ConfigValue to the expected OF type.
132
+ * Returns null if the type does not match (signals TYPE_MISMATCH).
133
+ */
134
+ _coerce(raw, expectedType, defaultValue) {
135
+ switch (expectedType) {
136
+ case "boolean":
137
+ if (typeof raw === "boolean") return raw;
138
+ return null;
139
+ case "string":
140
+ if (typeof raw === "string") return raw;
141
+ if (raw !== null && typeof raw === "object" && "seconds" in raw && "ms" in raw) {
142
+ return this._durationToISO(raw);
143
+ }
144
+ return null;
145
+ case "number":
146
+ if (typeof raw === "number") return raw;
147
+ return null;
148
+ case "object":
149
+ if (Array.isArray(raw)) return raw;
150
+ if (raw !== null && typeof raw === "object") return raw;
151
+ return null;
152
+ default:
153
+ return null;
154
+ }
155
+ }
156
+ /** Convert a Quonfig Duration to an ISO 8601 duration string (e.g. "PT1H30M"). */
157
+ _durationToISO(duration) {
158
+ const totalSeconds = duration.seconds;
159
+ const hours = Math.floor(totalSeconds / 3600);
160
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
161
+ const secs = totalSeconds % 60;
162
+ let iso = "PT";
163
+ if (hours > 0) iso += `${hours}H`;
164
+ if (minutes > 0) iso += `${minutes}M`;
165
+ if (secs > 0 || iso === "PT") iso += `${secs}S`;
166
+ return iso;
167
+ }
168
+ };
169
+ export {
170
+ QuonfigWebProvider,
171
+ mapContext,
172
+ toErrorCode
173
+ };
174
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/provider.ts","../src/context.ts","../src/errors.ts"],"sourcesContent":["import {\n ErrorCode,\n OpenFeatureEventEmitter,\n Provider,\n ResolutionDetails,\n StandardResolutionReasons,\n} from \"@openfeature/web-sdk\";\nimport type { EvaluationContext, JsonValue } from \"@openfeature/web-sdk\";\nimport { Quonfig } from \"@quonfig/javascript\";\n\nimport { mapContext } from \"./context\";\nimport { toErrorCode } from \"./errors\";\n\nexport interface QuonfigWebProviderOptions {\n sdkKey: string;\n /** Which Quonfig context property the OpenFeature targetingKey maps to. Default: \"user.id\" */\n targetingKeyMapping?: string;\n /** Override the Quonfig API base URL. */\n apiUrl?: string;\n /** Request timeout in ms. */\n timeout?: number;\n}\n\nexport class QuonfigWebProvider implements Provider {\n readonly metadata = { name: \"quonfig-web\" } as const;\n readonly runsOn = \"client\" as const;\n hooks = [];\n readonly events = new OpenFeatureEventEmitter();\n\n private client: Quonfig;\n private readonly targetingKeyMapping: string;\n private readonly sdkKey: string;\n private readonly apiUrl: string | undefined;\n private readonly timeout: number | undefined;\n\n constructor(options: QuonfigWebProviderOptions) {\n this.sdkKey = options.sdkKey;\n this.targetingKeyMapping = options.targetingKeyMapping ?? \"user.id\";\n this.apiUrl = options.apiUrl;\n this.timeout = options.timeout;\n this.client = new Quonfig();\n }\n\n async initialize(context?: EvaluationContext): Promise<void> {\n const nativeCtx = context\n ? mapContext(context, this.targetingKeyMapping)\n : { \"\": {} };\n\n await this.client.init({\n sdkKey: this.sdkKey,\n context: nativeCtx,\n ...(this.apiUrl !== undefined && { apiUrl: this.apiUrl }),\n ...(this.timeout !== undefined && { timeout: this.timeout }),\n });\n }\n\n async onContextChanged(\n _oldCtx: EvaluationContext,\n newCtx: EvaluationContext\n ): Promise<void> {\n const nativeCtx = mapContext(newCtx, this.targetingKeyMapping);\n await this.client.updateContext(nativeCtx);\n }\n\n async shutdown(): Promise<void> {\n this.client.close();\n }\n\n resolveBooleanEvaluation(\n flagKey: string,\n defaultValue: boolean,\n _context?: EvaluationContext\n ): ResolutionDetails<boolean> {\n return this._resolve(flagKey, defaultValue, \"boolean\");\n }\n\n resolveStringEvaluation(\n flagKey: string,\n defaultValue: string,\n _context?: EvaluationContext\n ): ResolutionDetails<string> {\n return this._resolve(flagKey, defaultValue, \"string\");\n }\n\n resolveNumberEvaluation(\n flagKey: string,\n defaultValue: number,\n _context?: EvaluationContext\n ): ResolutionDetails<number> {\n return this._resolve(flagKey, defaultValue, \"number\");\n }\n\n resolveObjectEvaluation<T extends JsonValue = JsonValue>(\n flagKey: string,\n defaultValue: T,\n _context?: EvaluationContext\n ): ResolutionDetails<T> {\n return this._resolve(flagKey, defaultValue, \"object\");\n }\n\n /** Escape hatch: access the underlying Quonfig client directly. */\n getClient(): Quonfig {\n return this.client;\n }\n\n // ---------------------------------------------------------------------------\n // Private helpers\n // ---------------------------------------------------------------------------\n\n private _resolve<T>(\n flagKey: string,\n defaultValue: T,\n expectedType: \"boolean\" | \"string\" | \"number\" | \"object\"\n ): ResolutionDetails<T> {\n try {\n const raw = this.client.get(flagKey);\n\n if (raw === undefined || raw === null) {\n // Flag not found — return OF default\n return {\n value: defaultValue,\n reason: StandardResolutionReasons.DEFAULT,\n errorCode: ErrorCode.FLAG_NOT_FOUND,\n };\n }\n\n // Type coercion / validation\n const coerced = this._coerce<T>(raw, expectedType, defaultValue);\n if (coerced === null) {\n return {\n value: defaultValue,\n reason: StandardResolutionReasons.ERROR,\n errorCode: ErrorCode.TYPE_MISMATCH,\n };\n }\n\n return {\n value: coerced,\n reason: StandardResolutionReasons.STATIC,\n };\n } catch (err) {\n return {\n value: defaultValue,\n reason: StandardResolutionReasons.ERROR,\n errorCode: toErrorCode(err),\n };\n }\n }\n\n /**\n * Coerce a raw ConfigValue to the expected OF type.\n * Returns null if the type does not match (signals TYPE_MISMATCH).\n */\n private _coerce<T>(\n raw: unknown,\n expectedType: \"boolean\" | \"string\" | \"number\" | \"object\",\n defaultValue: T\n ): T | null {\n switch (expectedType) {\n case \"boolean\":\n if (typeof raw === \"boolean\") return raw as unknown as T;\n return null;\n\n case \"string\":\n if (typeof raw === \"string\") return raw as unknown as T;\n // Duration objects: return ISO 8601 string representation\n if (\n raw !== null &&\n typeof raw === \"object\" &&\n \"seconds\" in raw &&\n \"ms\" in raw\n ) {\n return this._durationToISO(raw as { seconds: number; ms: number }) as unknown as T;\n }\n return null;\n\n case \"number\":\n if (typeof raw === \"number\") return raw as unknown as T;\n return null;\n\n case \"object\":\n // Arrays (string_list) and plain objects both satisfy \"object\"\n if (Array.isArray(raw)) return raw as unknown as T;\n if (raw !== null && typeof raw === \"object\") return raw as unknown as T;\n return null;\n\n default:\n return null;\n }\n }\n\n /** Convert a Quonfig Duration to an ISO 8601 duration string (e.g. \"PT1H30M\"). */\n private _durationToISO(duration: { seconds: number; ms: number }): string {\n const totalSeconds = duration.seconds;\n const hours = Math.floor(totalSeconds / 3600);\n const minutes = Math.floor((totalSeconds % 3600) / 60);\n const secs = totalSeconds % 60;\n\n let iso = \"PT\";\n if (hours > 0) iso += `${hours}H`;\n if (minutes > 0) iso += `${minutes}M`;\n if (secs > 0 || iso === \"PT\") iso += `${secs}S`;\n return iso;\n }\n}\n","import type { EvaluationContext } from \"@openfeature/web-sdk\";\nimport type { Contexts } from \"@quonfig/javascript\";\n\n/**\n * Map an OpenFeature EvaluationContext to Quonfig's nested Contexts format.\n *\n * Rules:\n * - `targetingKey` maps to the property specified by `targetingKeyMapping` (default: \"user.id\")\n * - Keys with a dot are split on the FIRST dot: namespace = left side, key = right side\n * - Keys without a dot go into the default (\"\") namespace\n */\nexport function mapContext(\n ofContext: EvaluationContext,\n targetingKeyMapping = \"user.id\"\n): Contexts {\n const result: Record<string, Record<string, unknown>> = {};\n\n for (const [key, value] of Object.entries(ofContext)) {\n if (value === undefined) continue;\n\n if (key === \"targetingKey\") {\n const dotIdx = targetingKeyMapping.indexOf(\".\");\n const ns = dotIdx === -1 ? \"\" : targetingKeyMapping.slice(0, dotIdx);\n const prop =\n dotIdx === -1 ? targetingKeyMapping : targetingKeyMapping.slice(dotIdx + 1);\n result[ns] ??= {};\n result[ns][prop] = value;\n continue;\n }\n\n const dotIdx = key.indexOf(\".\");\n if (dotIdx === -1) {\n result[\"\"] ??= {};\n result[\"\"][key] = value;\n } else {\n const ns = key.slice(0, dotIdx);\n const prop = key.slice(dotIdx + 1);\n result[ns] ??= {};\n result[ns][prop] = value;\n }\n }\n\n return result as Contexts;\n}\n","import { ErrorCode } from \"@openfeature/web-sdk\";\n\n/**\n * Map a native SDK error (or unknown throw) to an OpenFeature ErrorCode.\n */\nexport function toErrorCode(err: unknown): ErrorCode {\n const msg =\n err instanceof Error ? err.message.toLowerCase() : String(err).toLowerCase();\n\n if (msg.includes(\"flag not found\") || msg.includes(\"not found\")) {\n return ErrorCode.FLAG_NOT_FOUND;\n }\n if (msg.includes(\"type mismatch\") || msg.includes(\"type_mismatch\")) {\n return ErrorCode.TYPE_MISMATCH;\n }\n if (\n msg.includes(\"not initialized\") ||\n msg.includes(\"provider_not_ready\") ||\n msg.includes(\"call init()\") ||\n msg.includes(\"not ready\")\n ) {\n return ErrorCode.PROVIDER_NOT_READY;\n }\n return ErrorCode.GENERAL;\n}\n"],"mappings":"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","names":["ErrorCode","dotIdx","ErrorCode"]}
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@quonfig/openfeature-web",
3
+ "version": "0.0.1",
4
+ "description": "OpenFeature provider for Quonfig — Web/Browser (also works with @openfeature/react-sdk)",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "scripts": {
19
+ "build": "rm -rf dist/ && npx tsup",
20
+ "test": "vitest run --exclude test/conformance/**",
21
+ "test:conformance": "vitest run test/conformance",
22
+ "lint": "tsc --noEmit",
23
+ "prepublishOnly": "npm run build"
24
+ },
25
+ "peerDependencies": {
26
+ "@quonfig/javascript": ">=0.0.3",
27
+ "@openfeature/web-sdk": ">=1.0.0"
28
+ },
29
+ "devDependencies": {
30
+ "@quonfig/javascript": "^0.0.3",
31
+ "@openfeature/web-sdk": "^1.0.0",
32
+ "@types/node": "^20.11.0",
33
+ "tsup": "^8.0.0",
34
+ "typescript": "^5.3.0",
35
+ "vitest": "^1.0.0"
36
+ },
37
+ "publishConfig": {
38
+ "access": "public"
39
+ },
40
+ "author": "Quonfig",
41
+ "license": "MIT",
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "https://github.com/quonfig/openfeature-web.git"
45
+ }
46
+ }