@quonfig/openfeature-web 0.0.9 → 1.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 CHANGED
@@ -104,14 +104,14 @@ features require `provider.getClient()` (the escape hatch):
104
104
  2. **`string_list` configs** — access via `getObjectValue` and cast to `string[]`
105
105
  3. **`duration` configs** — `getStringValue` returns an ISO 8601 string; parse client-side
106
106
  4. **`bytes` configs** — not accessible via OpenFeature
107
- 5. **`keys()` and `raw()`** — native SDK only
107
+ 5. **Bulk config access** (`extract()`, the `configs` getter, `get()`) — native SDK only
108
108
  6. **Context keys** must use dot-notation (`"user.email"`), not nested objects
109
109
  7. **`targetingKey`** maps to `user.id` by default — configure `targetingKeyMapping` if different
110
110
 
111
111
  ```typescript
112
112
  // Escape hatch for Quonfig-native features
113
113
  const native = provider.getClient();
114
- native.shouldLog({ loggerName: "auth", desiredLevel: "DEBUG", defaultLevel: "WARN" });
114
+ native.shouldLog({ configKey: "log-level.auth", desiredLevel: "DEBUG", defaultLevel: "WARN" });
115
115
  ```
116
116
 
117
117
  ## License
package/dist/index.d.mts CHANGED
@@ -38,7 +38,12 @@ declare class QuonfigWebProvider implements Provider {
38
38
  * Returns null if the type does not match (signals TYPE_MISMATCH).
39
39
  */
40
40
  private _coerce;
41
- /** Convert a Quonfig Duration to an ISO 8601 duration string (e.g. "PT1H30M"). */
41
+ /**
42
+ * Convert a Quonfig Duration to its canonical ISO 8601 string in the Quonfig
43
+ * duration grammar (e.g. "P1DT6H2M1.5S"): days kept as D, zero components
44
+ * omitted, "PT0S" for zero. Works from the integer ms count, so the output
45
+ * carries no float residue.
46
+ */
42
47
  private _durationToISO;
43
48
  }
44
49
 
package/dist/index.d.ts CHANGED
@@ -38,7 +38,12 @@ declare class QuonfigWebProvider implements Provider {
38
38
  * Returns null if the type does not match (signals TYPE_MISMATCH).
39
39
  */
40
40
  private _coerce;
41
- /** Convert a Quonfig Duration to an ISO 8601 duration string (e.g. "PT1H30M"). */
41
+ /**
42
+ * Convert a Quonfig Duration to its canonical ISO 8601 string in the Quonfig
43
+ * duration grammar (e.g. "P1DT6H2M1.5S"): days kept as D, zero components
44
+ * omitted, "PT0S" for zero. Works from the integer ms count, so the output
45
+ * carries no float residue.
46
+ */
42
47
  private _durationToISO;
43
48
  }
44
49
 
package/dist/index.js CHANGED
@@ -189,17 +189,31 @@ var QuonfigWebProvider = class {
189
189
  return null;
190
190
  }
191
191
  }
192
- /** Convert a Quonfig Duration to an ISO 8601 duration string (e.g. "PT1H30M"). */
192
+ /**
193
+ * Convert a Quonfig Duration to its canonical ISO 8601 string in the Quonfig
194
+ * duration grammar (e.g. "P1DT6H2M1.5S"): days kept as D, zero components
195
+ * omitted, "PT0S" for zero. Works from the integer ms count, so the output
196
+ * carries no float residue.
197
+ */
193
198
  _durationToISO(duration) {
194
- const totalSeconds = duration.seconds;
195
- const hours = Math.floor(totalSeconds / 3600);
196
- const minutes = Math.floor(totalSeconds % 3600 / 60);
197
- const secs = totalSeconds % 60;
198
- let iso = "PT";
199
- if (hours > 0) iso += `${hours}H`;
200
- if (minutes > 0) iso += `${minutes}M`;
201
- if (secs > 0 || iso === "PT") iso += `${secs}S`;
202
- return iso;
199
+ let rest = Math.round(duration.ms);
200
+ const days = Math.floor(rest / 864e5);
201
+ rest -= days * 864e5;
202
+ const hours = Math.floor(rest / 36e5);
203
+ rest -= hours * 36e5;
204
+ const minutes = Math.floor(rest / 6e4);
205
+ rest -= minutes * 6e4;
206
+ const secs = Math.floor(rest / 1e3);
207
+ const millis = rest - secs * 1e3;
208
+ let time = "";
209
+ if (hours > 0) time += `${hours}H`;
210
+ if (minutes > 0) time += `${minutes}M`;
211
+ if (secs > 0 || millis > 0) {
212
+ const fraction = millis > 0 ? `.${String(millis).padStart(3, "0").replace(/0+$/, "")}` : "";
213
+ time += `${secs}${fraction}S`;
214
+ }
215
+ if (days === 0 && time === "") return "PT0S";
216
+ return `P${days > 0 ? `${days}D` : ""}${time ? `T${time}` : ""}`;
203
217
  }
204
218
  };
205
219
  function toOFReason(reason) {
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, 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"]}
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 /**\n * Convert a Quonfig Duration to its canonical ISO 8601 string in the Quonfig\n * duration grammar (e.g. \"P1DT6H2M1.5S\"): days kept as D, zero components\n * omitted, \"PT0S\" for zero. Works from the integer ms count, so the output\n * carries no float residue.\n */\n private _durationToISO(duration: { seconds: number; ms: number }): string {\n let rest = Math.round(duration.ms);\n const days = Math.floor(rest / 86400000);\n rest -= days * 86400000;\n const hours = Math.floor(rest / 3600000);\n rest -= hours * 3600000;\n const minutes = Math.floor(rest / 60000);\n rest -= minutes * 60000;\n const secs = Math.floor(rest / 1000);\n const millis = rest - secs * 1000;\n\n let time = \"\";\n if (hours > 0) time += `${hours}H`;\n if (minutes > 0) time += `${minutes}M`;\n if (secs > 0 || millis > 0) {\n const fraction = millis > 0 ? `.${String(millis).padStart(3, \"0\").replace(/0+$/, \"\")}` : \"\";\n time += `${secs}${fraction}S`;\n }\n\n if (days === 0 && time === \"\") return \"PT0S\";\n return `P${days > 0 ? `${days}D` : \"\"}${time ? `T${time}` : \"\"}`;\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
@@ -165,17 +165,31 @@ var QuonfigWebProvider = class {
165
165
  return null;
166
166
  }
167
167
  }
168
- /** Convert a Quonfig Duration to an ISO 8601 duration string (e.g. "PT1H30M"). */
168
+ /**
169
+ * Convert a Quonfig Duration to its canonical ISO 8601 string in the Quonfig
170
+ * duration grammar (e.g. "P1DT6H2M1.5S"): days kept as D, zero components
171
+ * omitted, "PT0S" for zero. Works from the integer ms count, so the output
172
+ * carries no float residue.
173
+ */
169
174
  _durationToISO(duration) {
170
- const totalSeconds = duration.seconds;
171
- const hours = Math.floor(totalSeconds / 3600);
172
- const minutes = Math.floor(totalSeconds % 3600 / 60);
173
- const secs = totalSeconds % 60;
174
- let iso = "PT";
175
- if (hours > 0) iso += `${hours}H`;
176
- if (minutes > 0) iso += `${minutes}M`;
177
- if (secs > 0 || iso === "PT") iso += `${secs}S`;
178
- return iso;
175
+ let rest = Math.round(duration.ms);
176
+ const days = Math.floor(rest / 864e5);
177
+ rest -= days * 864e5;
178
+ const hours = Math.floor(rest / 36e5);
179
+ rest -= hours * 36e5;
180
+ const minutes = Math.floor(rest / 6e4);
181
+ rest -= minutes * 6e4;
182
+ const secs = Math.floor(rest / 1e3);
183
+ const millis = rest - secs * 1e3;
184
+ let time = "";
185
+ if (hours > 0) time += `${hours}H`;
186
+ if (minutes > 0) time += `${minutes}M`;
187
+ if (secs > 0 || millis > 0) {
188
+ const fraction = millis > 0 ? `.${String(millis).padStart(3, "0").replace(/0+$/, "")}` : "";
189
+ time += `${secs}${fraction}S`;
190
+ }
191
+ if (days === 0 && time === "") return "PT0S";
192
+ return `P${days > 0 ? `${days}D` : ""}${time ? `T${time}` : ""}`;
179
193
  }
180
194
  };
181
195
  function toOFReason(reason) {
@@ -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, 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"]}
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 /**\n * Convert a Quonfig Duration to its canonical ISO 8601 string in the Quonfig\n * duration grammar (e.g. \"P1DT6H2M1.5S\"): days kept as D, zero components\n * omitted, \"PT0S\" for zero. Works from the integer ms count, so the output\n * carries no float residue.\n */\n private _durationToISO(duration: { seconds: number; ms: number }): string {\n let rest = Math.round(duration.ms);\n const days = Math.floor(rest / 86400000);\n rest -= days * 86400000;\n const hours = Math.floor(rest / 3600000);\n rest -= hours * 3600000;\n const minutes = Math.floor(rest / 60000);\n rest -= minutes * 60000;\n const secs = Math.floor(rest / 1000);\n const millis = rest - secs * 1000;\n\n let time = \"\";\n if (hours > 0) time += `${hours}H`;\n if (minutes > 0) time += `${minutes}M`;\n if (secs > 0 || millis > 0) {\n const fraction = millis > 0 ? `.${String(millis).padStart(3, \"0\").replace(/0+$/, \"\")}` : \"\";\n time += `${secs}${fraction}S`;\n }\n\n if (days === 0 && time === \"\") return \"PT0S\";\n return `P${days > 0 ? `${days}D` : \"\"}${time ? `T${time}` : \"\"}`;\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.9",
3
+ "version": "1.0.1",
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",
@@ -26,11 +26,11 @@
26
26
  "version": "prettier --write CHANGELOG.md README.md && git add CHANGELOG.md README.md"
27
27
  },
28
28
  "peerDependencies": {
29
- "@quonfig/javascript": ">=0.0.18",
29
+ "@quonfig/javascript": ">=1.0.0",
30
30
  "@openfeature/web-sdk": ">=1.0.0"
31
31
  },
32
32
  "devDependencies": {
33
- "@quonfig/javascript": "^0.0.18",
33
+ "@quonfig/javascript": "^1.0.0",
34
34
  "@openfeature/web-sdk": "^1.0.0",
35
35
  "@types/node": "^20.11.0",
36
36
  "prettier": "^3.0.0",