@quonfig/openfeature-web 0.0.6 → 0.0.8

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 CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  OpenFeature provider for [Quonfig](https://quonfig.com) — Web/Browser.
4
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.
5
+ Works with both vanilla JS (`@openfeature/web-sdk`) and React (`@openfeature/react-sdk`). The React
6
+ SDK re-exports the web SDK and adds hooks (`useFlag`, `useBooleanFlagValue`, etc.) — any web
7
+ provider works with React hooks automatically.
8
8
 
9
9
  ## Installation
10
10
 
@@ -68,24 +68,24 @@ function App() {
68
68
 
69
69
  ```typescript
70
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
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
75
  });
76
76
  ```
77
77
 
78
78
  ## Context mapping
79
79
 
80
- OpenFeature uses a flat context; Quonfig uses a namespace-nested context.
81
- The provider maps between them using dot-notation:
80
+ OpenFeature uses a flat context; Quonfig uses a namespace-nested context. The provider maps between
81
+ them using dot-notation:
82
82
 
83
- | OpenFeature key | Quonfig context |
84
- |-------------------------|-----------------------------|
85
- | `targetingKey: "u-123"` | `{ user: { id: "u-123" } }` |
83
+ | OpenFeature key | Quonfig context |
84
+ | ----------------------- | ------------------------------ |
85
+ | `targetingKey: "u-123"` | `{ user: { id: "u-123" } }` |
86
86
  | `"user.email": "a@b.c"` | `{ user: { email: "a@b.c" } }` |
87
- | `"org.tier": "pro"` | `{ org: { tier: "pro" } }` |
88
- | `"country": "US"` | `{ "": { country: "US" } }` |
87
+ | `"org.tier": "pro"` | `{ org: { tier: "pro" } }` |
88
+ | `"country": "US"` | `{ "": { country: "US" } }` |
89
89
 
90
90
  Keys without a dot go into the default (empty-string) namespace.
91
91
 
@@ -97,8 +97,8 @@ new QuonfigWebProvider({ sdkKey: "...", targetingKeyMapping: "account.id" });
97
97
 
98
98
  ## What you lose vs. the native SDK
99
99
 
100
- The OpenFeature interface covers boolean, string, number, and object types.
101
- Some Quonfig-native features require `provider.getClient()` (the escape hatch):
100
+ The OpenFeature interface covers boolean, string, number, and object types. Some Quonfig-native
101
+ features require `provider.getClient()` (the escape hatch):
102
102
 
103
103
  1. **Log levels** (`shouldLog`, `logger`) — native SDK only
104
104
  2. **`string_list` configs** — access via `getObjectValue` and cast to `string[]`
package/dist/index.js CHANGED
@@ -122,34 +122,46 @@ var QuonfigWebProvider = class {
122
122
  // Private helpers
123
123
  // ---------------------------------------------------------------------------
124
124
  _resolve(flagKey, defaultValue, expectedType) {
125
+ let details;
125
126
  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
- }
127
+ details = this.client.getDetails(flagKey);
128
+ } catch (err) {
142
129
  return {
143
- value: coerced,
144
- reason: import_web_sdk2.StandardResolutionReasons.STATIC
130
+ value: defaultValue,
131
+ reason: import_web_sdk2.StandardResolutionReasons.ERROR,
132
+ errorCode: toErrorCode(err),
133
+ variant: "default",
134
+ flagMetadata: {}
145
135
  };
146
- } catch (err) {
136
+ }
137
+ if (details.reason === "ERROR") {
138
+ return {
139
+ value: defaultValue,
140
+ reason: import_web_sdk2.StandardResolutionReasons.ERROR,
141
+ errorCode: toOFErrorCode(details.errorCode),
142
+ ...details.errorMessage ? { errorMessage: details.errorMessage } : {},
143
+ variant: details.variant,
144
+ flagMetadata: details.flagMetadata
145
+ };
146
+ }
147
+ const coerced = this._coerce(details.value, expectedType, defaultValue);
148
+ if (coerced === null) {
147
149
  return {
148
150
  value: defaultValue,
149
151
  reason: import_web_sdk2.StandardResolutionReasons.ERROR,
150
- errorCode: toErrorCode(err)
152
+ errorCode: import_web_sdk2.ErrorCode.TYPE_MISMATCH,
153
+ // OF spec convention: variant='default' on the error path; preserve
154
+ // flagMetadata so consumers still see configId/configType.
155
+ variant: "default",
156
+ flagMetadata: details.flagMetadata
151
157
  };
152
158
  }
159
+ return {
160
+ value: coerced,
161
+ reason: toOFReason(details.reason),
162
+ variant: details.variant,
163
+ flagMetadata: details.flagMetadata
164
+ };
153
165
  }
154
166
  /**
155
167
  * Coerce a raw ConfigValue to the expected OF type.
@@ -190,6 +202,32 @@ var QuonfigWebProvider = class {
190
202
  return iso;
191
203
  }
192
204
  };
205
+ function toOFReason(reason) {
206
+ switch (reason) {
207
+ case "STATIC":
208
+ return import_web_sdk2.StandardResolutionReasons.STATIC;
209
+ case "TARGETING_MATCH":
210
+ return import_web_sdk2.StandardResolutionReasons.TARGETING_MATCH;
211
+ case "SPLIT":
212
+ return import_web_sdk2.StandardResolutionReasons.SPLIT;
213
+ case "DEFAULT":
214
+ return import_web_sdk2.StandardResolutionReasons.DEFAULT;
215
+ case "ERROR":
216
+ default:
217
+ return import_web_sdk2.StandardResolutionReasons.ERROR;
218
+ }
219
+ }
220
+ function toOFErrorCode(code) {
221
+ switch (code) {
222
+ case "FLAG_NOT_FOUND":
223
+ return import_web_sdk2.ErrorCode.FLAG_NOT_FOUND;
224
+ case "TYPE_MISMATCH":
225
+ return import_web_sdk2.ErrorCode.TYPE_MISMATCH;
226
+ case "GENERAL":
227
+ default:
228
+ return import_web_sdk2.ErrorCode.GENERAL;
229
+ }
230
+ }
193
231
  // Annotate the CommonJS export names for ESM import in node:
194
232
  0 && (module.exports = {
195
233
  QuonfigWebProvider,
package/dist/index.js.map CHANGED
@@ -1 +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 await 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"]}
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, ResolutionReason } from \"@openfeature/web-sdk\";\nimport { Quonfig } from \"@quonfig/javascript\";\nimport type { EvaluationDetails } 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 ? mapContext(context, this.targetingKeyMapping) : { \"\": {} };\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(_oldCtx: EvaluationContext, newCtx: EvaluationContext): Promise<void> {\n const nativeCtx = mapContext(newCtx, this.targetingKeyMapping);\n await this.client.updateContext(nativeCtx);\n }\n\n async shutdown(): Promise<void> {\n await 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 let details: EvaluationDetails;\n try {\n details = this.client.getDetails(flagKey);\n } catch (err) {\n return {\n value: defaultValue,\n reason: StandardResolutionReasons.ERROR,\n errorCode: toErrorCode(err),\n variant: \"default\",\n flagMetadata: {},\n };\n }\n\n // Errors from the SDK side (FLAG_NOT_FOUND, GENERAL) — pass through.\n if (details.reason === \"ERROR\") {\n return {\n value: defaultValue,\n reason: StandardResolutionReasons.ERROR,\n errorCode: toOFErrorCode(details.errorCode),\n ...(details.errorMessage ? { errorMessage: details.errorMessage } : {}),\n variant: details.variant,\n flagMetadata: details.flagMetadata as ResolutionDetails<T>[\"flagMetadata\"],\n };\n }\n\n // Coerce the resolved value to the expected OF shape. A null result\n // signals provider-level TYPE_MISMATCH (the SDK has no requested-type\n // hint, so this happens here, not inside getDetails).\n const coerced = this._coerce<T>(details.value, expectedType, defaultValue);\n if (coerced === null) {\n return {\n value: defaultValue,\n reason: StandardResolutionReasons.ERROR,\n errorCode: ErrorCode.TYPE_MISMATCH,\n // OF spec convention: variant='default' on the error path; preserve\n // flagMetadata so consumers still see configId/configType.\n variant: \"default\",\n flagMetadata: details.flagMetadata as ResolutionDetails<T>[\"flagMetadata\"],\n };\n }\n\n return {\n value: coerced,\n reason: toOFReason(details.reason),\n variant: details.variant,\n flagMetadata: details.flagMetadata as ResolutionDetails<T>[\"flagMetadata\"],\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 (raw !== null && typeof raw === \"object\" && \"seconds\" in raw && \"ms\" in raw) {\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\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Map sdk-javascript's EvaluationReason onto OpenFeature's StandardResolutionReasons.\n * SPLIT is provider-defined; OF allows arbitrary strings, so the literal value works.\n */\nfunction toOFReason(reason: EvaluationDetails[\"reason\"]): ResolutionReason {\n switch (reason) {\n case \"STATIC\":\n return StandardResolutionReasons.STATIC;\n case \"TARGETING_MATCH\":\n return StandardResolutionReasons.TARGETING_MATCH;\n case \"SPLIT\":\n return StandardResolutionReasons.SPLIT;\n case \"DEFAULT\":\n return StandardResolutionReasons.DEFAULT;\n case \"ERROR\":\n default:\n return StandardResolutionReasons.ERROR;\n }\n}\n\n/** Translate sdk-javascript's EvaluationErrorCode onto the OF ErrorCode enum. */\nfunction toOFErrorCode(code: EvaluationDetails[\"errorCode\"]): ErrorCode {\n switch (code) {\n case \"FLAG_NOT_FOUND\":\n return ErrorCode.FLAG_NOT_FOUND;\n case \"TYPE_MISMATCH\":\n return ErrorCode.TYPE_MISMATCH;\n case \"GENERAL\":\n default:\n return ErrorCode.GENERAL;\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 = 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 = 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 CHANGED
@@ -98,34 +98,46 @@ var QuonfigWebProvider = class {
98
98
  // Private helpers
99
99
  // ---------------------------------------------------------------------------
100
100
  _resolve(flagKey, defaultValue, expectedType) {
101
+ let details;
101
102
  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
- }
103
+ details = this.client.getDetails(flagKey);
104
+ } catch (err) {
118
105
  return {
119
- value: coerced,
120
- reason: StandardResolutionReasons.STATIC
106
+ value: defaultValue,
107
+ reason: StandardResolutionReasons.ERROR,
108
+ errorCode: toErrorCode(err),
109
+ variant: "default",
110
+ flagMetadata: {}
121
111
  };
122
- } catch (err) {
112
+ }
113
+ if (details.reason === "ERROR") {
114
+ return {
115
+ value: defaultValue,
116
+ reason: StandardResolutionReasons.ERROR,
117
+ errorCode: toOFErrorCode(details.errorCode),
118
+ ...details.errorMessage ? { errorMessage: details.errorMessage } : {},
119
+ variant: details.variant,
120
+ flagMetadata: details.flagMetadata
121
+ };
122
+ }
123
+ const coerced = this._coerce(details.value, expectedType, defaultValue);
124
+ if (coerced === null) {
123
125
  return {
124
126
  value: defaultValue,
125
127
  reason: StandardResolutionReasons.ERROR,
126
- errorCode: toErrorCode(err)
128
+ errorCode: ErrorCode2.TYPE_MISMATCH,
129
+ // OF spec convention: variant='default' on the error path; preserve
130
+ // flagMetadata so consumers still see configId/configType.
131
+ variant: "default",
132
+ flagMetadata: details.flagMetadata
127
133
  };
128
134
  }
135
+ return {
136
+ value: coerced,
137
+ reason: toOFReason(details.reason),
138
+ variant: details.variant,
139
+ flagMetadata: details.flagMetadata
140
+ };
129
141
  }
130
142
  /**
131
143
  * Coerce a raw ConfigValue to the expected OF type.
@@ -166,6 +178,32 @@ var QuonfigWebProvider = class {
166
178
  return iso;
167
179
  }
168
180
  };
181
+ function toOFReason(reason) {
182
+ switch (reason) {
183
+ case "STATIC":
184
+ return StandardResolutionReasons.STATIC;
185
+ case "TARGETING_MATCH":
186
+ return StandardResolutionReasons.TARGETING_MATCH;
187
+ case "SPLIT":
188
+ return StandardResolutionReasons.SPLIT;
189
+ case "DEFAULT":
190
+ return StandardResolutionReasons.DEFAULT;
191
+ case "ERROR":
192
+ default:
193
+ return StandardResolutionReasons.ERROR;
194
+ }
195
+ }
196
+ function toOFErrorCode(code) {
197
+ switch (code) {
198
+ case "FLAG_NOT_FOUND":
199
+ return ErrorCode2.FLAG_NOT_FOUND;
200
+ case "TYPE_MISMATCH":
201
+ return ErrorCode2.TYPE_MISMATCH;
202
+ case "GENERAL":
203
+ default:
204
+ return ErrorCode2.GENERAL;
205
+ }
206
+ }
169
207
  export {
170
208
  QuonfigWebProvider,
171
209
  mapContext,
@@ -1 +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 await 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":";AAAA;AAAA,EACE,aAAAA;AAAA,EACA;AAAA,EAGA;AAAA,OACK;AAEP,SAAS,eAAe;;;ACGjB,SAAS,WACd,WACA,sBAAsB,WACZ;AACV,QAAM,SAAkD,CAAC;AAEzD,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,SAAS,GAAG;AACpD,QAAI,UAAU,OAAW;AAEzB,QAAI,QAAQ,gBAAgB;AAC1B,YAAMC,UAAS,oBAAoB,QAAQ,GAAG;AAC9C,YAAM,KAAKA,YAAW,KAAK,KAAK,oBAAoB,MAAM,GAAGA,OAAM;AACnE,YAAM,OACJA,YAAW,KAAK,sBAAsB,oBAAoB,MAAMA,UAAS,CAAC;AAC5E,kCAAe,CAAC;AAChB,aAAO,EAAE,EAAE,IAAI,IAAI;AACnB;AAAA,IACF;AAEA,UAAM,SAAS,IAAI,QAAQ,GAAG;AAC9B,QAAI,WAAW,IAAI;AACjB,kCAAe,CAAC;AAChB,aAAO,EAAE,EAAE,GAAG,IAAI;AAAA,IACpB,OAAO;AACL,YAAM,KAAK,IAAI,MAAM,GAAG,MAAM;AAC9B,YAAM,OAAO,IAAI,MAAM,SAAS,CAAC;AACjC,kCAAe,CAAC;AAChB,aAAO,EAAE,EAAE,IAAI,IAAI;AAAA,IACrB;AAAA,EACF;AAEA,SAAO;AACT;;;AC3CA,SAAS,iBAAiB;AAKnB,SAAS,YAAY,KAAyB;AACnD,QAAM,MACJ,eAAe,QAAQ,IAAI,QAAQ,YAAY,IAAI,OAAO,GAAG,EAAE,YAAY;AAE7E,MAAI,IAAI,SAAS,gBAAgB,KAAK,IAAI,SAAS,WAAW,GAAG;AAC/D,WAAO,UAAU;AAAA,EACnB;AACA,MAAI,IAAI,SAAS,eAAe,KAAK,IAAI,SAAS,eAAe,GAAG;AAClE,WAAO,UAAU;AAAA,EACnB;AACA,MACE,IAAI,SAAS,iBAAiB,KAC9B,IAAI,SAAS,oBAAoB,KACjC,IAAI,SAAS,aAAa,KAC1B,IAAI,SAAS,WAAW,GACxB;AACA,WAAO,UAAU;AAAA,EACnB;AACA,SAAO,UAAU;AACnB;;;AFDO,IAAM,qBAAN,MAA6C;AAAA,EAYlD,YAAY,SAAoC;AAXhD,SAAS,WAAW,EAAE,MAAM,cAAc;AAC1C,SAAS,SAAS;AAClB,iBAAQ,CAAC;AACT,SAAS,SAAS,IAAI,wBAAwB;AAS5C,SAAK,SAAS,QAAQ;AACtB,SAAK,sBAAsB,QAAQ,uBAAuB;AAC1D,SAAK,SAAS,QAAQ;AACtB,SAAK,UAAU,QAAQ;AACvB,SAAK,SAAS,IAAI,QAAQ;AAAA,EAC5B;AAAA,EAEA,MAAM,WAAW,SAA4C;AAC3D,UAAM,YAAY,UACd,WAAW,SAAS,KAAK,mBAAmB,IAC5C,EAAE,IAAI,CAAC,EAAE;AAEb,UAAM,KAAK,OAAO,KAAK;AAAA,MACrB,QAAQ,KAAK;AAAA,MACb,SAAS;AAAA,MACT,GAAI,KAAK,WAAW,UAAa,EAAE,QAAQ,KAAK,OAAO;AAAA,MACvD,GAAI,KAAK,YAAY,UAAa,EAAE,SAAS,KAAK,QAAQ;AAAA,IAC5D,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,iBACJ,SACA,QACe;AACf,UAAM,YAAY,WAAW,QAAQ,KAAK,mBAAmB;AAC7D,UAAM,KAAK,OAAO,cAAc,SAAS;AAAA,EAC3C;AAAA,EAEA,MAAM,WAA0B;AAC9B,UAAM,KAAK,OAAO,MAAM;AAAA,EAC1B;AAAA,EAEA,yBACE,SACA,cACA,UAC4B;AAC5B,WAAO,KAAK,SAAS,SAAS,cAAc,SAAS;AAAA,EACvD;AAAA,EAEA,wBACE,SACA,cACA,UAC2B;AAC3B,WAAO,KAAK,SAAS,SAAS,cAAc,QAAQ;AAAA,EACtD;AAAA,EAEA,wBACE,SACA,cACA,UAC2B;AAC3B,WAAO,KAAK,SAAS,SAAS,cAAc,QAAQ;AAAA,EACtD;AAAA,EAEA,wBACE,SACA,cACA,UACsB;AACtB,WAAO,KAAK,SAAS,SAAS,cAAc,QAAQ;AAAA,EACtD;AAAA;AAAA,EAGA,YAAqB;AACnB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAMQ,SACN,SACA,cACA,cACsB;AACtB,QAAI;AACF,YAAM,MAAM,KAAK,OAAO,IAAI,OAAO;AAEnC,UAAI,QAAQ,UAAa,QAAQ,MAAM;AAErC,eAAO;AAAA,UACL,OAAO;AAAA,UACP,QAAQ,0BAA0B;AAAA,UAClC,WAAWC,WAAU;AAAA,QACvB;AAAA,MACF;AAGA,YAAM,UAAU,KAAK,QAAW,KAAK,cAAc,YAAY;AAC/D,UAAI,YAAY,MAAM;AACpB,eAAO;AAAA,UACL,OAAO;AAAA,UACP,QAAQ,0BAA0B;AAAA,UAClC,WAAWA,WAAU;AAAA,QACvB;AAAA,MACF;AAEA,aAAO;AAAA,QACL,OAAO;AAAA,QACP,QAAQ,0BAA0B;AAAA,MACpC;AAAA,IACF,SAAS,KAAK;AACZ,aAAO;AAAA,QACL,OAAO;AAAA,QACP,QAAQ,0BAA0B;AAAA,QAClC,WAAW,YAAY,GAAG;AAAA,MAC5B;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,QACN,KACA,cACA,cACU;AACV,YAAQ,cAAc;AAAA,MACpB,KAAK;AACH,YAAI,OAAO,QAAQ,UAAW,QAAO;AACrC,eAAO;AAAA,MAET,KAAK;AACH,YAAI,OAAO,QAAQ,SAAU,QAAO;AAEpC,YACE,QAAQ,QACR,OAAO,QAAQ,YACf,aAAa,OACb,QAAQ,KACR;AACA,iBAAO,KAAK,eAAe,GAAsC;AAAA,QACnE;AACA,eAAO;AAAA,MAET,KAAK;AACH,YAAI,OAAO,QAAQ,SAAU,QAAO;AACpC,eAAO;AAAA,MAET,KAAK;AAEH,YAAI,MAAM,QAAQ,GAAG,EAAG,QAAO;AAC/B,YAAI,QAAQ,QAAQ,OAAO,QAAQ,SAAU,QAAO;AACpD,eAAO;AAAA,MAET;AACE,eAAO;AAAA,IACX;AAAA,EACF;AAAA;AAAA,EAGQ,eAAe,UAAmD;AACxE,UAAM,eAAe,SAAS;AAC9B,UAAM,QAAQ,KAAK,MAAM,eAAe,IAAI;AAC5C,UAAM,UAAU,KAAK,MAAO,eAAe,OAAQ,EAAE;AACrD,UAAM,OAAO,eAAe;AAE5B,QAAI,MAAM;AACV,QAAI,QAAQ,EAAG,QAAO,GAAG,KAAK;AAC9B,QAAI,UAAU,EAAG,QAAO,GAAG,OAAO;AAClC,QAAI,OAAO,KAAK,QAAQ,KAAM,QAAO,GAAG,IAAI;AAC5C,WAAO;AAAA,EACT;AACF;","names":["ErrorCode","dotIdx","ErrorCode"]}
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, ResolutionReason } from \"@openfeature/web-sdk\";\nimport { Quonfig } from \"@quonfig/javascript\";\nimport type { EvaluationDetails } 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 ? mapContext(context, this.targetingKeyMapping) : { \"\": {} };\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(_oldCtx: EvaluationContext, newCtx: EvaluationContext): Promise<void> {\n const nativeCtx = mapContext(newCtx, this.targetingKeyMapping);\n await this.client.updateContext(nativeCtx);\n }\n\n async shutdown(): Promise<void> {\n await 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 let details: EvaluationDetails;\n try {\n details = this.client.getDetails(flagKey);\n } catch (err) {\n return {\n value: defaultValue,\n reason: StandardResolutionReasons.ERROR,\n errorCode: toErrorCode(err),\n variant: \"default\",\n flagMetadata: {},\n };\n }\n\n // Errors from the SDK side (FLAG_NOT_FOUND, GENERAL) — pass through.\n if (details.reason === \"ERROR\") {\n return {\n value: defaultValue,\n reason: StandardResolutionReasons.ERROR,\n errorCode: toOFErrorCode(details.errorCode),\n ...(details.errorMessage ? { errorMessage: details.errorMessage } : {}),\n variant: details.variant,\n flagMetadata: details.flagMetadata as ResolutionDetails<T>[\"flagMetadata\"],\n };\n }\n\n // Coerce the resolved value to the expected OF shape. A null result\n // signals provider-level TYPE_MISMATCH (the SDK has no requested-type\n // hint, so this happens here, not inside getDetails).\n const coerced = this._coerce<T>(details.value, expectedType, defaultValue);\n if (coerced === null) {\n return {\n value: defaultValue,\n reason: StandardResolutionReasons.ERROR,\n errorCode: ErrorCode.TYPE_MISMATCH,\n // OF spec convention: variant='default' on the error path; preserve\n // flagMetadata so consumers still see configId/configType.\n variant: \"default\",\n flagMetadata: details.flagMetadata as ResolutionDetails<T>[\"flagMetadata\"],\n };\n }\n\n return {\n value: coerced,\n reason: toOFReason(details.reason),\n variant: details.variant,\n flagMetadata: details.flagMetadata as ResolutionDetails<T>[\"flagMetadata\"],\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 (raw !== null && typeof raw === \"object\" && \"seconds\" in raw && \"ms\" in raw) {\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\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Map sdk-javascript's EvaluationReason onto OpenFeature's StandardResolutionReasons.\n * SPLIT is provider-defined; OF allows arbitrary strings, so the literal value works.\n */\nfunction toOFReason(reason: EvaluationDetails[\"reason\"]): ResolutionReason {\n switch (reason) {\n case \"STATIC\":\n return StandardResolutionReasons.STATIC;\n case \"TARGETING_MATCH\":\n return StandardResolutionReasons.TARGETING_MATCH;\n case \"SPLIT\":\n return StandardResolutionReasons.SPLIT;\n case \"DEFAULT\":\n return StandardResolutionReasons.DEFAULT;\n case \"ERROR\":\n default:\n return StandardResolutionReasons.ERROR;\n }\n}\n\n/** Translate sdk-javascript's EvaluationErrorCode onto the OF ErrorCode enum. */\nfunction toOFErrorCode(code: EvaluationDetails[\"errorCode\"]): ErrorCode {\n switch (code) {\n case \"FLAG_NOT_FOUND\":\n return ErrorCode.FLAG_NOT_FOUND;\n case \"TYPE_MISMATCH\":\n return ErrorCode.TYPE_MISMATCH;\n case \"GENERAL\":\n default:\n return ErrorCode.GENERAL;\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 = 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 = 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quonfig/openfeature-web",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
4
4
  "description": "OpenFeature provider for Quonfig — Web/Browser (also works with @openfeature/react-sdk)",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -20,16 +20,20 @@
20
20
  "test": "vitest run --exclude test/conformance/**",
21
21
  "test:conformance": "vitest run test/conformance",
22
22
  "lint": "tsc --noEmit",
23
- "prepublishOnly": "npm run build"
23
+ "prepublishOnly": "npm run build",
24
+ "prettier": "prettier . -l",
25
+ "prettier:fix": "prettier --write .",
26
+ "version": "prettier --write CHANGELOG.md README.md && git add CHANGELOG.md README.md"
24
27
  },
25
28
  "peerDependencies": {
26
- "@quonfig/javascript": ">=0.0.15",
29
+ "@quonfig/javascript": ">=0.0.17",
27
30
  "@openfeature/web-sdk": ">=1.0.0"
28
31
  },
29
32
  "devDependencies": {
30
- "@quonfig/javascript": "^0.0.15",
33
+ "@quonfig/javascript": "^0.0.17",
31
34
  "@openfeature/web-sdk": "^1.0.0",
32
35
  "@types/node": "^20.11.0",
36
+ "prettier": "^3.0.0",
33
37
  "tsup": "^8.0.0",
34
38
  "typescript": "^5.3.0",
35
39
  "vitest": "^1.0.0"