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