org.inovus.flags 1.0.0

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/LICENSE ADDED
@@ -0,0 +1,17 @@
1
+ Closed Source. Proprietary & Confidential.
2
+
3
+ Copyright (c) 2026 Inovus Ltd. All rights reserved.
4
+
5
+ This software and associated documentation (the "Software") are the
6
+ proprietary and confidential property of Inovus Ltd.
7
+
8
+ No part of the Software may be used, copied, modified, merged, published,
9
+ distributed, sublicensed, or sold without prior written permission from
10
+ Inovus Ltd.
11
+
12
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
13
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
14
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
15
+ INOVUS LTD BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
16
+ AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
17
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,354 @@
1
+ # org.inovus.flags
2
+
3
+ TypeScript client for Totum feature flags. Call `init()` once at startup, then read flags **synchronously** from memory — with background refresh and safe fallbacks when the network is unavailable.
4
+
5
+ **Zero runtime dependencies.** Requires global `fetch` (Node 18+, browsers, Workers).
6
+
7
+ ---
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install org.inovus.flags
13
+ ```
14
+
15
+ ```typescript
16
+ import { createFlagsClient } from 'org.inovus.flags';
17
+ import type {
18
+ FlagDeclaration,
19
+ EvaluationContext,
20
+ FlagsClient,
21
+ FlagType,
22
+ } from 'org.inovus.flags';
23
+ ```
24
+
25
+ ---
26
+
27
+ ## Prerequisites
28
+
29
+ Your platform team provides two values — add them to `.env`, hosting secrets, or CI:
30
+
31
+ | Variable | Purpose |
32
+ |----------|---------|
33
+ | `FLAGS_PROXY_URL` | Base URL of the flags HTTP API (no trailing slash required) |
34
+ | `FLAGS_API_KEY` | API key sent as header `X-Flags-Api-Key` |
35
+
36
+ ```env
37
+ FLAGS_PROXY_URL=https://flags.example.com
38
+ FLAGS_API_KEY=your-proxy-key
39
+ ```
40
+
41
+ **Do not** put Cloudflare account tokens or other infrastructure credentials in application code.
42
+
43
+ **Next.js:** use `.env.local` / project settings. Prefix with `NEXT_PUBLIC_` only if the browser bundle must read them (both values will be visible in the client — the proxy key is a limited gate, not an account secret).
44
+
45
+ ---
46
+
47
+ ## Quick start
48
+
49
+ ```typescript
50
+ import { createFlagsClient } from 'org.inovus.flags';
51
+
52
+ const flags = createFlagsClient({
53
+ proxyUrl: process.env.FLAGS_PROXY_URL!,
54
+ apiKey: process.env.FLAGS_API_KEY!,
55
+ });
56
+
57
+ await flags.init([
58
+ { flagKey: 'my-app-dark-mode', type: 'boolean', defaultValue: false },
59
+ { flagKey: 'my-app-checkout-flow', type: 'string', defaultValue: 'v1' },
60
+ ]);
61
+
62
+ // Synchronous — no await on getters
63
+ const darkMode = flags.getBoolean('my-app-dark-mode', false);
64
+ const checkout = flags.getString('my-app-checkout-flow', 'v1');
65
+ ```
66
+
67
+ ---
68
+
69
+ ## Flag keys
70
+
71
+ Use one shared flag platform for the organisation. Separate products by **flag key prefix** (hyphen-separated):
72
+
73
+ - `totum-admin-dark-mode`
74
+ - `mobile-checkout-flow`
75
+ - `video-uploader-metrics-enabled`
76
+
77
+ Flag keys allow **letters, numbers, and hyphens only** — no dots.
78
+
79
+ Each `flagKey` in code must exist in the flag dashboard with the **matching type** (`boolean`, `string`, `number`, `object`).
80
+
81
+ ### Dashboard: “Enabled” is not the same as `true`
82
+
83
+ | Control | Meaning | Typical API `reason` |
84
+ |---------|---------|----------------------|
85
+ | **Enabled** (toggle) | Flag is active and evaluated | If off → `DISABLED` |
86
+ | **Default** (variant) | Value when no targeting rules match | `DEFAULT` |
87
+
88
+ **Enabled does not force boolean `true`.** A flag can be Enabled with **Default = `false`** and no rules — the API correctly returns `false` with `"reason":"DEFAULT"`.
89
+
90
+ To enable a feature for **everyone**, set **Default** to the `true` variant. For partial rollouts, add targeting rules and pass matching `context` on `init` / `refresh`.
91
+
92
+ | `reason` | Meaning |
93
+ |----------|---------|
94
+ | `DEFAULT` | No rule matched; default variant served |
95
+ | `DISABLED` | Flag toggle off |
96
+ | `TARGETING_MATCH` | A targeting rule matched |
97
+ | `SPLIT` | Percentage / rollout bucket |
98
+ | `ERROR` | Evaluation problem — check `errorCode` in the response |
99
+
100
+ ---
101
+
102
+ ## Integration pattern (required)
103
+
104
+ 1. **One client** per app (singleton or React context).
105
+ 2. **`await flags.init(declarations[, context])`** once before relying on flag values — list **every** flag the app reads.
106
+ 3. **Synchronous reads:** `getBoolean`, `getString`, `getNumber`, `getObject` — **never** `await` these.
107
+ 4. **`await flags.refresh(context?)`** after login or when targeting attributes change (`userId`, `plan`, etc.).
108
+ 5. **`flags.destroy()`** optional — stops the background refresh timer on app teardown.
109
+
110
+ ### Targeting context
111
+
112
+ ```typescript
113
+ await flags.init(
114
+ [{ flagKey: 'my-app-dark-mode', type: 'boolean', defaultValue: false }],
115
+ { userId: 'anonymous' }
116
+ );
117
+
118
+ await flags.refresh({ userId: user.id, plan: user.plan });
119
+ ```
120
+
121
+ ### React (sketch)
122
+
123
+ ```typescript
124
+ import { createFlagsClient, type FlagDeclaration } from 'org.inovus.flags';
125
+
126
+ const declarations: FlagDeclaration[] = [
127
+ { flagKey: 'my-app-dark-mode', type: 'boolean', defaultValue: false },
128
+ ];
129
+
130
+ export const flags = createFlagsClient({
131
+ proxyUrl: process.env.NEXT_PUBLIC_FLAGS_PROXY_URL!,
132
+ apiKey: process.env.NEXT_PUBLIC_FLAGS_API_KEY!,
133
+ });
134
+
135
+ let initPromise: Promise<void> | null = null;
136
+
137
+ export function ensureFlagsInit(context?: EvaluationContext): Promise<void> {
138
+ if (!initPromise) initPromise = flags.init(declarations, context);
139
+ return initPromise;
140
+ }
141
+ ```
142
+
143
+ Gate UI until `ensureFlagsInit()` resolves, or accept `defaultValue` until loaded.
144
+
145
+ ### Node backend
146
+
147
+ ```typescript
148
+ const flags = createFlagsClient({
149
+ proxyUrl: process.env.FLAGS_PROXY_URL!,
150
+ apiKey: process.env.FLAGS_API_KEY!,
151
+ persist: false,
152
+ });
153
+
154
+ await flags.init(declarations);
155
+ await flags.refresh({ userId: req.user.id });
156
+ ```
157
+
158
+ ---
159
+
160
+ ## Client options
161
+
162
+ | Option | Required | Default | Description |
163
+ |--------|----------|---------|-------------|
164
+ | `proxyUrl` | Yes | — | Flags API base URL |
165
+ | `apiKey` | Yes | — | `X-Flags-Api-Key` header value |
166
+ | `ttl` | No | `30000` | Background refresh interval (ms) |
167
+ | `persist` | No | `true` | Persist cache to `localStorage` in browsers (`flags_cache` key) |
168
+
169
+ ---
170
+
171
+ ## API reference
172
+
173
+ ### `createFlagsClient(options): FlagsClient`
174
+
175
+ ### `init(declarations, context?): Promise<void>`
176
+
177
+ - **declarations:** `{ flagKey, type, defaultValue }[]`
178
+ - **type:** `'boolean' | 'string' | 'number' | 'object'`
179
+ - One batch HTTP request; starts background refresh
180
+ - Does not throw on network failure (logs warnings)
181
+
182
+ ### `refresh(context?): Promise<void>`
183
+
184
+ Re-fetches declared flags. On failure, **keeps** existing cache.
185
+
186
+ ### `getBoolean` / `getString` / `getNumber` / `getObject`
187
+
188
+ Synchronous. Never throws. Wrong or missing key → `defaultValue` + console warning.
189
+
190
+ ### `destroy(): void`
191
+
192
+ Stops background interval.
193
+
194
+ ### Types
195
+
196
+ ```typescript
197
+ type FlagType = 'boolean' | 'string' | 'number' | 'object';
198
+ type EvaluationContext = Record<string, string | number | boolean>;
199
+
200
+ interface FlagDeclaration {
201
+ flagKey: string;
202
+ type: FlagType;
203
+ defaultValue: unknown;
204
+ }
205
+ ```
206
+
207
+ ---
208
+
209
+ ## HTTP API (what the SDK calls)
210
+
211
+ ### Authentication
212
+
213
+ ```http
214
+ X-Flags-Api-Key: <FLAGS_API_KEY>
215
+ ```
216
+
217
+ ### `GET /health`
218
+
219
+ No auth. `{ "status": "ok" }`
220
+
221
+ ### `POST /evaluate/batch`
222
+
223
+ Auth required. Body: non-empty array of:
224
+
225
+ ```json
226
+ {
227
+ "flagKey": "my-app-dark-mode",
228
+ "type": "boolean",
229
+ "defaultValue": false,
230
+ "context": { "userId": "user-123", "plan": "premium" }
231
+ }
232
+ ```
233
+
234
+ Response `200`: array of `{ flagKey, value, reason }` in request order.
235
+
236
+ ### `401 Unauthorized`
237
+
238
+ Missing or invalid API key.
239
+
240
+ ---
241
+
242
+ ## Caching and failures
243
+
244
+ **Fallback order** (first match wins):
245
+
246
+ 1. Fresh batch response
247
+ 2. In-memory cache (stale is OK)
248
+ 3. `localStorage` `flags_cache` (browsers, if `persist: true`)
249
+ 4. `defaultValue` passed to `getX()`
250
+
251
+ **Console warnings** (prefix `[org.inovus.flags]`):
252
+
253
+ - `init: Worker unreachable and no persisted cache; getX() will use defaults`
254
+ - `refresh: failed; keeping existing cache`
255
+ - `get: flag "..." not in cache; using defaultValue`
256
+
257
+ ---
258
+
259
+ ## Anti-patterns
260
+
261
+ | Wrong | Correct |
262
+ |-------|---------|
263
+ | Infrastructure credentials in the app | `FLAGS_PROXY_URL` + `FLAGS_API_KEY` only |
264
+ | `await flags.getBoolean(...)` | `flags.getBoolean(...)` |
265
+ | HTTP request per flag per render | `init()` once; sync reads |
266
+ | Flag not listed in `init()` | Declare every flag in `init()` |
267
+ | Direct calls to the flag provider API | Use this SDK only |
268
+ | Reset UI to defaults when refresh fails | SDK keeps cache |
269
+ | Skip `await init()` | Await `init()` or gate UI |
270
+
271
+ ---
272
+
273
+ ## New flag checklist
274
+
275
+ 1. Create the flag in the dashboard with the correct type and **Default** variant.
276
+ 2. Add to `init([..., { flagKey, type, defaultValue }])`.
277
+ 3. Read with the matching `getX()` and the same `flagKey`.
278
+ 4. Add targeting via `context` on `init` / `refresh` if needed.
279
+
280
+ ---
281
+
282
+ ## Troubleshooting
283
+
284
+ | Symptom | Action |
285
+ |---------|--------|
286
+ | Always `defaultValue` | `await init()`; verify `GET /health` |
287
+ | HTTP 401 | Check `FLAGS_API_KEY` with platform team |
288
+ | Wrong value | Align dashboard type and `init` declaration |
289
+ | Stale after login | `await refresh({ userId, plan, ... })` |
290
+ | Tests flaky | `persist: false` in test setup |
291
+ | `reason: "ERROR"` | Flag missing or type mismatch in dashboard |
292
+ | Enabled in UI but `false` + `DEFAULT` | Set **Default** variant to `true` or add targeting |
293
+
294
+ ### Verify connectivity (curl)
295
+
296
+ ```bash
297
+ curl "https://flags.example.com/health"
298
+ ```
299
+
300
+ ```bash
301
+ curl -X POST "https://flags.example.com/evaluate/batch" \
302
+ -H "Content-Type: application/json" \
303
+ -H "X-Flags-Api-Key: your-proxy-key" \
304
+ -d '[{"flagKey":"my-app-dark-mode","type":"boolean","defaultValue":false}]'
305
+ ```
306
+
307
+ **Windows cmd** (escape `"` inside `-d` as `\"`):
308
+
309
+ ```bat
310
+ curl "https://flags.example.com/health"
311
+ curl -X POST "https://flags.example.com/evaluate/batch" -H "Content-Type: application/json" -H "X-Flags-Api-Key: your-proxy-key" -d "[{\"flagKey\":\"my-app-dark-mode\",\"type\":\"boolean\",\"defaultValue\":false}]"
312
+ ```
313
+
314
+ ---
315
+
316
+ ## App template
317
+
318
+ ```typescript
319
+ import { createFlagsClient, type FlagDeclaration } from 'org.inovus.flags';
320
+
321
+ const DECLARATIONS: FlagDeclaration[] = [
322
+ // { flagKey: 'my-app-feature-x', type: 'boolean', defaultValue: false },
323
+ ];
324
+
325
+ export const flags = createFlagsClient({
326
+ proxyUrl: process.env.FLAGS_PROXY_URL!,
327
+ apiKey: process.env.FLAGS_API_KEY!,
328
+ persist: typeof window !== 'undefined',
329
+ });
330
+
331
+ let ready: Promise<void> | null = null;
332
+
333
+ export function initFlags(context?: Record<string, string | number | boolean>) {
334
+ if (!ready) ready = flags.init(DECLARATIONS, context);
335
+ return ready;
336
+ }
337
+ ```
338
+
339
+ ---
340
+
341
+ ## AI agents
342
+
343
+ When implementing feature flags in an application:
344
+
345
+ - Follow the **integration pattern** and **anti-patterns** above.
346
+ - Use only `FLAGS_PROXY_URL` and `FLAGS_API_KEY` from the environment.
347
+ - List every flag in `init()` before calling `getX()`.
348
+ - Do not bypass this SDK for routine flag reads.
349
+
350
+ ---
351
+
352
+ ## License
353
+
354
+ Closed Source. Proprietary & Confidential. See `LICENSE`.
package/dist/index.cjs ADDED
@@ -0,0 +1,203 @@
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
+ FlagsCache: () => FlagsCache,
24
+ createFlagsClient: () => createFlagsClient
25
+ });
26
+ module.exports = __toCommonJS(index_exports);
27
+
28
+ // src/cache.ts
29
+ var FlagsCache = class {
30
+ constructor(ttl, persist) {
31
+ this.ttl = ttl;
32
+ this.persist = persist;
33
+ this.values = {};
34
+ this.timestamp = 0;
35
+ this.load();
36
+ }
37
+ set(values) {
38
+ this.values = values;
39
+ this.timestamp = Date.now();
40
+ if (this.persist) this.save();
41
+ }
42
+ get(flagKey) {
43
+ return this.values[flagKey];
44
+ }
45
+ isStale() {
46
+ return this.timestamp === 0 || Date.now() - this.timestamp > this.ttl;
47
+ }
48
+ clear() {
49
+ this.values = {};
50
+ this.timestamp = 0;
51
+ if (this.persist && typeof localStorage !== "undefined") {
52
+ localStorage.removeItem("flags_cache");
53
+ }
54
+ }
55
+ save() {
56
+ if (typeof localStorage === "undefined") return;
57
+ try {
58
+ localStorage.setItem(
59
+ "flags_cache",
60
+ JSON.stringify({
61
+ values: this.values,
62
+ timestamp: this.timestamp
63
+ })
64
+ );
65
+ } catch {
66
+ }
67
+ }
68
+ load() {
69
+ if (!this.persist || typeof localStorage === "undefined") return;
70
+ try {
71
+ const raw = localStorage.getItem("flags_cache");
72
+ if (!raw) return;
73
+ const parsed = JSON.parse(raw);
74
+ if (parsed?.values && typeof parsed.timestamp === "number") {
75
+ this.values = parsed.values;
76
+ this.timestamp = parsed.timestamp;
77
+ }
78
+ } catch {
79
+ }
80
+ }
81
+ };
82
+
83
+ // src/client.ts
84
+ var DEFAULT_TTL = 3e4;
85
+ function warn(message) {
86
+ if (typeof console !== "undefined" && console.warn) {
87
+ console.warn(`[org.inovus.flags] ${message}`);
88
+ }
89
+ }
90
+ var FlagsClientImpl = class {
91
+ constructor(options) {
92
+ this.declarations = [];
93
+ this.proxyUrl = options.proxyUrl.replace(/\/$/, "");
94
+ this.apiKey = options.apiKey;
95
+ this.ttl = options.ttl ?? DEFAULT_TTL;
96
+ const persist = options.persist ?? true;
97
+ this.cache = new FlagsCache(this.ttl, persist);
98
+ }
99
+ async init(declarations, context) {
100
+ this.declarations = declarations;
101
+ this.context = context;
102
+ const ok = await this.fetchAndCache(context);
103
+ if (!ok) {
104
+ const hasCache = declarations.some((d) => this.cache.get(d.flagKey) !== void 0);
105
+ if (!hasCache) {
106
+ warn("init: Worker unreachable and no persisted cache; getX() will use defaults");
107
+ }
108
+ }
109
+ this.startBackgroundRefresh();
110
+ }
111
+ async refresh(context) {
112
+ if (context !== void 0) {
113
+ this.context = context;
114
+ }
115
+ const ok = await this.fetchAndCache(this.context);
116
+ if (!ok) {
117
+ warn("refresh: failed; keeping existing cache");
118
+ }
119
+ }
120
+ getBoolean(flagKey, defaultValue) {
121
+ return this.read(flagKey, defaultValue, (v) => typeof v === "boolean");
122
+ }
123
+ getString(flagKey, defaultValue) {
124
+ return this.read(flagKey, defaultValue, (v) => typeof v === "string");
125
+ }
126
+ getNumber(flagKey, defaultValue) {
127
+ return this.read(flagKey, defaultValue, (v) => typeof v === "number");
128
+ }
129
+ getObject(flagKey, defaultValue) {
130
+ return this.read(flagKey, defaultValue, (v) => v !== null && typeof v === "object");
131
+ }
132
+ destroy() {
133
+ if (this.refreshTimer !== void 0) {
134
+ clearInterval(this.refreshTimer);
135
+ this.refreshTimer = void 0;
136
+ }
137
+ }
138
+ read(flagKey, defaultValue, guard) {
139
+ const cached = this.cache.get(flagKey);
140
+ if (cached === void 0) {
141
+ warn(`get: flag "${flagKey}" not in cache; using defaultValue`);
142
+ return defaultValue;
143
+ }
144
+ if (!guard(cached)) {
145
+ warn(`get: flag "${flagKey}" has wrong type in cache; using defaultValue`);
146
+ return defaultValue;
147
+ }
148
+ return cached;
149
+ }
150
+ startBackgroundRefresh() {
151
+ this.destroy();
152
+ this.refreshTimer = setInterval(() => {
153
+ void this.refresh();
154
+ }, this.ttl);
155
+ }
156
+ async fetchAndCache(context) {
157
+ if (this.declarations.length === 0) return true;
158
+ const body = this.declarations.map((d) => ({
159
+ flagKey: d.flagKey,
160
+ type: d.type,
161
+ defaultValue: d.defaultValue,
162
+ ...context ? { context } : {}
163
+ }));
164
+ try {
165
+ const response = await fetch(`${this.proxyUrl}/evaluate/batch`, {
166
+ method: "POST",
167
+ headers: {
168
+ "Content-Type": "application/json",
169
+ "X-Flags-Api-Key": this.apiKey
170
+ },
171
+ body: JSON.stringify(body)
172
+ });
173
+ if (!response.ok) {
174
+ return false;
175
+ }
176
+ const results = await response.json();
177
+ if (!Array.isArray(results)) {
178
+ return false;
179
+ }
180
+ const values = {};
181
+ for (const result of results) {
182
+ if (result && typeof result.flagKey === "string") {
183
+ values[result.flagKey] = result.value;
184
+ }
185
+ }
186
+ this.cache.set(values);
187
+ return true;
188
+ } catch {
189
+ return false;
190
+ }
191
+ }
192
+ };
193
+
194
+ // src/index.ts
195
+ function createFlagsClient(options) {
196
+ return new FlagsClientImpl(options);
197
+ }
198
+ // Annotate the CommonJS export names for ESM import in node:
199
+ 0 && (module.exports = {
200
+ FlagsCache,
201
+ createFlagsClient
202
+ });
203
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/cache.ts","../src/client.ts"],"sourcesContent":["import { FlagsClientImpl } from './client';\nimport type { FlagsClient, FlagsClientOptions } from './types';\n\nexport function createFlagsClient(options: FlagsClientOptions): FlagsClient {\n return new FlagsClientImpl(options);\n}\n\nexport type {\n CachedFlagStore,\n EvaluationContext,\n EvaluationRequest,\n EvaluationResult,\n FlagDeclaration,\n FlagType,\n FlagsClient,\n FlagsClientOptions,\n} from './types';\n\nexport { FlagsCache } from './cache';\n","export class FlagsCache {\n private values: Record<string, unknown> = {};\n private timestamp = 0;\n\n constructor(\n private ttl: number,\n private persist: boolean\n ) {\n this.load();\n }\n\n set(values: Record<string, unknown>): void {\n this.values = values;\n this.timestamp = Date.now();\n if (this.persist) this.save();\n }\n\n get(flagKey: string): unknown | undefined {\n return this.values[flagKey];\n }\n\n isStale(): boolean {\n return this.timestamp === 0 || Date.now() - this.timestamp > this.ttl;\n }\n\n clear(): void {\n this.values = {};\n this.timestamp = 0;\n if (this.persist && typeof localStorage !== 'undefined') {\n localStorage.removeItem('flags_cache');\n }\n }\n\n private save(): void {\n if (typeof localStorage === 'undefined') return;\n try {\n localStorage.setItem(\n 'flags_cache',\n JSON.stringify({\n values: this.values,\n timestamp: this.timestamp,\n })\n );\n } catch {\n /* quota exceeded */\n }\n }\n\n private load(): void {\n if (!this.persist || typeof localStorage === 'undefined') return;\n try {\n const raw = localStorage.getItem('flags_cache');\n if (!raw) return;\n const parsed = JSON.parse(raw);\n if (parsed?.values && typeof parsed.timestamp === 'number') {\n this.values = parsed.values;\n this.timestamp = parsed.timestamp;\n }\n } catch {\n /* malformed */\n }\n }\n}\n","import { FlagsCache } from './cache';\nimport type {\n EvaluationContext,\n EvaluationResult,\n FlagDeclaration,\n FlagsClient,\n FlagsClientOptions,\n} from './types';\n\nconst DEFAULT_TTL = 30_000;\n\nfunction warn(message: string): void {\n if (typeof console !== 'undefined' && console.warn) {\n console.warn(`[org.inovus.flags] ${message}`);\n }\n}\n\nexport class FlagsClientImpl implements FlagsClient {\n private readonly proxyUrl: string;\n private readonly apiKey: string;\n private readonly ttl: number;\n private readonly cache: FlagsCache;\n private declarations: FlagDeclaration[] = [];\n private context: EvaluationContext | undefined;\n private refreshTimer: ReturnType<typeof setInterval> | undefined;\n\n constructor(options: FlagsClientOptions) {\n this.proxyUrl = options.proxyUrl.replace(/\\/$/, '');\n this.apiKey = options.apiKey;\n this.ttl = options.ttl ?? DEFAULT_TTL;\n const persist = options.persist ?? true;\n this.cache = new FlagsCache(this.ttl, persist);\n }\n\n async init(declarations: FlagDeclaration[], context?: EvaluationContext): Promise<void> {\n this.declarations = declarations;\n this.context = context;\n const ok = await this.fetchAndCache(context);\n if (!ok) {\n const hasCache = declarations.some((d) => this.cache.get(d.flagKey) !== undefined);\n if (!hasCache) {\n warn('init: Worker unreachable and no persisted cache; getX() will use defaults');\n }\n }\n this.startBackgroundRefresh();\n }\n\n async refresh(context?: EvaluationContext): Promise<void> {\n if (context !== undefined) {\n this.context = context;\n }\n const ok = await this.fetchAndCache(this.context);\n if (!ok) {\n warn('refresh: failed; keeping existing cache');\n }\n }\n\n getBoolean(flagKey: string, defaultValue: boolean): boolean {\n return this.read(flagKey, defaultValue, (v): v is boolean => typeof v === 'boolean');\n }\n\n getString(flagKey: string, defaultValue: string): string {\n return this.read(flagKey, defaultValue, (v): v is string => typeof v === 'string');\n }\n\n getNumber(flagKey: string, defaultValue: number): number {\n return this.read(flagKey, defaultValue, (v): v is number => typeof v === 'number');\n }\n\n getObject<T>(flagKey: string, defaultValue: T): T {\n return this.read(flagKey, defaultValue, (v): v is T => v !== null && typeof v === 'object');\n }\n\n destroy(): void {\n if (this.refreshTimer !== undefined) {\n clearInterval(this.refreshTimer);\n this.refreshTimer = undefined;\n }\n }\n\n private read<T>(\n flagKey: string,\n defaultValue: T,\n guard: (value: unknown) => value is T\n ): T {\n const cached = this.cache.get(flagKey);\n if (cached === undefined) {\n warn(`get: flag \"${flagKey}\" not in cache; using defaultValue`);\n return defaultValue;\n }\n if (!guard(cached)) {\n warn(`get: flag \"${flagKey}\" has wrong type in cache; using defaultValue`);\n return defaultValue;\n }\n return cached;\n }\n\n private startBackgroundRefresh(): void {\n this.destroy();\n this.refreshTimer = setInterval(() => {\n void this.refresh();\n }, this.ttl);\n }\n\n private async fetchAndCache(context?: EvaluationContext): Promise<boolean> {\n if (this.declarations.length === 0) return true;\n\n const body = this.declarations.map((d) => ({\n flagKey: d.flagKey,\n type: d.type,\n defaultValue: d.defaultValue,\n ...(context ? { context } : {}),\n }));\n\n try {\n const response = await fetch(`${this.proxyUrl}/evaluate/batch`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-Flags-Api-Key': this.apiKey,\n },\n body: JSON.stringify(body),\n });\n\n if (!response.ok) {\n return false;\n }\n\n const results = (await response.json()) as EvaluationResult[];\n if (!Array.isArray(results)) {\n return false;\n }\n\n const values: Record<string, unknown> = {};\n for (const result of results) {\n if (result && typeof result.flagKey === 'string') {\n values[result.flagKey] = result.value;\n }\n }\n this.cache.set(values);\n return true;\n } catch {\n return false;\n }\n }\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,52 @@
1
+ type FlagType = 'boolean' | 'string' | 'number' | 'object';
2
+ type EvaluationContext = Record<string, string | number | boolean>;
3
+ interface FlagDeclaration {
4
+ flagKey: string;
5
+ type: FlagType;
6
+ defaultValue: unknown;
7
+ }
8
+ interface EvaluationRequest extends FlagDeclaration {
9
+ context?: EvaluationContext;
10
+ }
11
+ interface EvaluationResult<T = unknown> {
12
+ flagKey: string;
13
+ value: T;
14
+ reason: string;
15
+ }
16
+ interface CachedFlagStore {
17
+ values: Record<string, unknown>;
18
+ timestamp: number;
19
+ }
20
+ interface FlagsClientOptions {
21
+ proxyUrl: string;
22
+ apiKey: string;
23
+ ttl?: number;
24
+ persist?: boolean;
25
+ }
26
+ interface FlagsClient {
27
+ init(declarations: FlagDeclaration[], context?: EvaluationContext): Promise<void>;
28
+ refresh(context?: EvaluationContext): Promise<void>;
29
+ getBoolean(flagKey: string, defaultValue: boolean): boolean;
30
+ getString(flagKey: string, defaultValue: string): string;
31
+ getNumber(flagKey: string, defaultValue: number): number;
32
+ getObject<T>(flagKey: string, defaultValue: T): T;
33
+ destroy(): void;
34
+ }
35
+
36
+ declare class FlagsCache {
37
+ private ttl;
38
+ private persist;
39
+ private values;
40
+ private timestamp;
41
+ constructor(ttl: number, persist: boolean);
42
+ set(values: Record<string, unknown>): void;
43
+ get(flagKey: string): unknown | undefined;
44
+ isStale(): boolean;
45
+ clear(): void;
46
+ private save;
47
+ private load;
48
+ }
49
+
50
+ declare function createFlagsClient(options: FlagsClientOptions): FlagsClient;
51
+
52
+ export { type CachedFlagStore, type EvaluationContext, type EvaluationRequest, type EvaluationResult, type FlagDeclaration, type FlagType, FlagsCache, type FlagsClient, type FlagsClientOptions, createFlagsClient };
@@ -0,0 +1,52 @@
1
+ type FlagType = 'boolean' | 'string' | 'number' | 'object';
2
+ type EvaluationContext = Record<string, string | number | boolean>;
3
+ interface FlagDeclaration {
4
+ flagKey: string;
5
+ type: FlagType;
6
+ defaultValue: unknown;
7
+ }
8
+ interface EvaluationRequest extends FlagDeclaration {
9
+ context?: EvaluationContext;
10
+ }
11
+ interface EvaluationResult<T = unknown> {
12
+ flagKey: string;
13
+ value: T;
14
+ reason: string;
15
+ }
16
+ interface CachedFlagStore {
17
+ values: Record<string, unknown>;
18
+ timestamp: number;
19
+ }
20
+ interface FlagsClientOptions {
21
+ proxyUrl: string;
22
+ apiKey: string;
23
+ ttl?: number;
24
+ persist?: boolean;
25
+ }
26
+ interface FlagsClient {
27
+ init(declarations: FlagDeclaration[], context?: EvaluationContext): Promise<void>;
28
+ refresh(context?: EvaluationContext): Promise<void>;
29
+ getBoolean(flagKey: string, defaultValue: boolean): boolean;
30
+ getString(flagKey: string, defaultValue: string): string;
31
+ getNumber(flagKey: string, defaultValue: number): number;
32
+ getObject<T>(flagKey: string, defaultValue: T): T;
33
+ destroy(): void;
34
+ }
35
+
36
+ declare class FlagsCache {
37
+ private ttl;
38
+ private persist;
39
+ private values;
40
+ private timestamp;
41
+ constructor(ttl: number, persist: boolean);
42
+ set(values: Record<string, unknown>): void;
43
+ get(flagKey: string): unknown | undefined;
44
+ isStale(): boolean;
45
+ clear(): void;
46
+ private save;
47
+ private load;
48
+ }
49
+
50
+ declare function createFlagsClient(options: FlagsClientOptions): FlagsClient;
51
+
52
+ export { type CachedFlagStore, type EvaluationContext, type EvaluationRequest, type EvaluationResult, type FlagDeclaration, type FlagType, FlagsCache, type FlagsClient, type FlagsClientOptions, createFlagsClient };
package/dist/index.js ADDED
@@ -0,0 +1,175 @@
1
+ // src/cache.ts
2
+ var FlagsCache = class {
3
+ constructor(ttl, persist) {
4
+ this.ttl = ttl;
5
+ this.persist = persist;
6
+ this.values = {};
7
+ this.timestamp = 0;
8
+ this.load();
9
+ }
10
+ set(values) {
11
+ this.values = values;
12
+ this.timestamp = Date.now();
13
+ if (this.persist) this.save();
14
+ }
15
+ get(flagKey) {
16
+ return this.values[flagKey];
17
+ }
18
+ isStale() {
19
+ return this.timestamp === 0 || Date.now() - this.timestamp > this.ttl;
20
+ }
21
+ clear() {
22
+ this.values = {};
23
+ this.timestamp = 0;
24
+ if (this.persist && typeof localStorage !== "undefined") {
25
+ localStorage.removeItem("flags_cache");
26
+ }
27
+ }
28
+ save() {
29
+ if (typeof localStorage === "undefined") return;
30
+ try {
31
+ localStorage.setItem(
32
+ "flags_cache",
33
+ JSON.stringify({
34
+ values: this.values,
35
+ timestamp: this.timestamp
36
+ })
37
+ );
38
+ } catch {
39
+ }
40
+ }
41
+ load() {
42
+ if (!this.persist || typeof localStorage === "undefined") return;
43
+ try {
44
+ const raw = localStorage.getItem("flags_cache");
45
+ if (!raw) return;
46
+ const parsed = JSON.parse(raw);
47
+ if (parsed?.values && typeof parsed.timestamp === "number") {
48
+ this.values = parsed.values;
49
+ this.timestamp = parsed.timestamp;
50
+ }
51
+ } catch {
52
+ }
53
+ }
54
+ };
55
+
56
+ // src/client.ts
57
+ var DEFAULT_TTL = 3e4;
58
+ function warn(message) {
59
+ if (typeof console !== "undefined" && console.warn) {
60
+ console.warn(`[org.inovus.flags] ${message}`);
61
+ }
62
+ }
63
+ var FlagsClientImpl = class {
64
+ constructor(options) {
65
+ this.declarations = [];
66
+ this.proxyUrl = options.proxyUrl.replace(/\/$/, "");
67
+ this.apiKey = options.apiKey;
68
+ this.ttl = options.ttl ?? DEFAULT_TTL;
69
+ const persist = options.persist ?? true;
70
+ this.cache = new FlagsCache(this.ttl, persist);
71
+ }
72
+ async init(declarations, context) {
73
+ this.declarations = declarations;
74
+ this.context = context;
75
+ const ok = await this.fetchAndCache(context);
76
+ if (!ok) {
77
+ const hasCache = declarations.some((d) => this.cache.get(d.flagKey) !== void 0);
78
+ if (!hasCache) {
79
+ warn("init: Worker unreachable and no persisted cache; getX() will use defaults");
80
+ }
81
+ }
82
+ this.startBackgroundRefresh();
83
+ }
84
+ async refresh(context) {
85
+ if (context !== void 0) {
86
+ this.context = context;
87
+ }
88
+ const ok = await this.fetchAndCache(this.context);
89
+ if (!ok) {
90
+ warn("refresh: failed; keeping existing cache");
91
+ }
92
+ }
93
+ getBoolean(flagKey, defaultValue) {
94
+ return this.read(flagKey, defaultValue, (v) => typeof v === "boolean");
95
+ }
96
+ getString(flagKey, defaultValue) {
97
+ return this.read(flagKey, defaultValue, (v) => typeof v === "string");
98
+ }
99
+ getNumber(flagKey, defaultValue) {
100
+ return this.read(flagKey, defaultValue, (v) => typeof v === "number");
101
+ }
102
+ getObject(flagKey, defaultValue) {
103
+ return this.read(flagKey, defaultValue, (v) => v !== null && typeof v === "object");
104
+ }
105
+ destroy() {
106
+ if (this.refreshTimer !== void 0) {
107
+ clearInterval(this.refreshTimer);
108
+ this.refreshTimer = void 0;
109
+ }
110
+ }
111
+ read(flagKey, defaultValue, guard) {
112
+ const cached = this.cache.get(flagKey);
113
+ if (cached === void 0) {
114
+ warn(`get: flag "${flagKey}" not in cache; using defaultValue`);
115
+ return defaultValue;
116
+ }
117
+ if (!guard(cached)) {
118
+ warn(`get: flag "${flagKey}" has wrong type in cache; using defaultValue`);
119
+ return defaultValue;
120
+ }
121
+ return cached;
122
+ }
123
+ startBackgroundRefresh() {
124
+ this.destroy();
125
+ this.refreshTimer = setInterval(() => {
126
+ void this.refresh();
127
+ }, this.ttl);
128
+ }
129
+ async fetchAndCache(context) {
130
+ if (this.declarations.length === 0) return true;
131
+ const body = this.declarations.map((d) => ({
132
+ flagKey: d.flagKey,
133
+ type: d.type,
134
+ defaultValue: d.defaultValue,
135
+ ...context ? { context } : {}
136
+ }));
137
+ try {
138
+ const response = await fetch(`${this.proxyUrl}/evaluate/batch`, {
139
+ method: "POST",
140
+ headers: {
141
+ "Content-Type": "application/json",
142
+ "X-Flags-Api-Key": this.apiKey
143
+ },
144
+ body: JSON.stringify(body)
145
+ });
146
+ if (!response.ok) {
147
+ return false;
148
+ }
149
+ const results = await response.json();
150
+ if (!Array.isArray(results)) {
151
+ return false;
152
+ }
153
+ const values = {};
154
+ for (const result of results) {
155
+ if (result && typeof result.flagKey === "string") {
156
+ values[result.flagKey] = result.value;
157
+ }
158
+ }
159
+ this.cache.set(values);
160
+ return true;
161
+ } catch {
162
+ return false;
163
+ }
164
+ }
165
+ };
166
+
167
+ // src/index.ts
168
+ function createFlagsClient(options) {
169
+ return new FlagsClientImpl(options);
170
+ }
171
+ export {
172
+ FlagsCache,
173
+ createFlagsClient
174
+ };
175
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cache.ts","../src/client.ts","../src/index.ts"],"sourcesContent":["export class FlagsCache {\n private values: Record<string, unknown> = {};\n private timestamp = 0;\n\n constructor(\n private ttl: number,\n private persist: boolean\n ) {\n this.load();\n }\n\n set(values: Record<string, unknown>): void {\n this.values = values;\n this.timestamp = Date.now();\n if (this.persist) this.save();\n }\n\n get(flagKey: string): unknown | undefined {\n return this.values[flagKey];\n }\n\n isStale(): boolean {\n return this.timestamp === 0 || Date.now() - this.timestamp > this.ttl;\n }\n\n clear(): void {\n this.values = {};\n this.timestamp = 0;\n if (this.persist && typeof localStorage !== 'undefined') {\n localStorage.removeItem('flags_cache');\n }\n }\n\n private save(): void {\n if (typeof localStorage === 'undefined') return;\n try {\n localStorage.setItem(\n 'flags_cache',\n JSON.stringify({\n values: this.values,\n timestamp: this.timestamp,\n })\n );\n } catch {\n /* quota exceeded */\n }\n }\n\n private load(): void {\n if (!this.persist || typeof localStorage === 'undefined') return;\n try {\n const raw = localStorage.getItem('flags_cache');\n if (!raw) return;\n const parsed = JSON.parse(raw);\n if (parsed?.values && typeof parsed.timestamp === 'number') {\n this.values = parsed.values;\n this.timestamp = parsed.timestamp;\n }\n } catch {\n /* malformed */\n }\n }\n}\n","import { FlagsCache } from './cache';\nimport type {\n EvaluationContext,\n EvaluationResult,\n FlagDeclaration,\n FlagsClient,\n FlagsClientOptions,\n} from './types';\n\nconst DEFAULT_TTL = 30_000;\n\nfunction warn(message: string): void {\n if (typeof console !== 'undefined' && console.warn) {\n console.warn(`[org.inovus.flags] ${message}`);\n }\n}\n\nexport class FlagsClientImpl implements FlagsClient {\n private readonly proxyUrl: string;\n private readonly apiKey: string;\n private readonly ttl: number;\n private readonly cache: FlagsCache;\n private declarations: FlagDeclaration[] = [];\n private context: EvaluationContext | undefined;\n private refreshTimer: ReturnType<typeof setInterval> | undefined;\n\n constructor(options: FlagsClientOptions) {\n this.proxyUrl = options.proxyUrl.replace(/\\/$/, '');\n this.apiKey = options.apiKey;\n this.ttl = options.ttl ?? DEFAULT_TTL;\n const persist = options.persist ?? true;\n this.cache = new FlagsCache(this.ttl, persist);\n }\n\n async init(declarations: FlagDeclaration[], context?: EvaluationContext): Promise<void> {\n this.declarations = declarations;\n this.context = context;\n const ok = await this.fetchAndCache(context);\n if (!ok) {\n const hasCache = declarations.some((d) => this.cache.get(d.flagKey) !== undefined);\n if (!hasCache) {\n warn('init: Worker unreachable and no persisted cache; getX() will use defaults');\n }\n }\n this.startBackgroundRefresh();\n }\n\n async refresh(context?: EvaluationContext): Promise<void> {\n if (context !== undefined) {\n this.context = context;\n }\n const ok = await this.fetchAndCache(this.context);\n if (!ok) {\n warn('refresh: failed; keeping existing cache');\n }\n }\n\n getBoolean(flagKey: string, defaultValue: boolean): boolean {\n return this.read(flagKey, defaultValue, (v): v is boolean => typeof v === 'boolean');\n }\n\n getString(flagKey: string, defaultValue: string): string {\n return this.read(flagKey, defaultValue, (v): v is string => typeof v === 'string');\n }\n\n getNumber(flagKey: string, defaultValue: number): number {\n return this.read(flagKey, defaultValue, (v): v is number => typeof v === 'number');\n }\n\n getObject<T>(flagKey: string, defaultValue: T): T {\n return this.read(flagKey, defaultValue, (v): v is T => v !== null && typeof v === 'object');\n }\n\n destroy(): void {\n if (this.refreshTimer !== undefined) {\n clearInterval(this.refreshTimer);\n this.refreshTimer = undefined;\n }\n }\n\n private read<T>(\n flagKey: string,\n defaultValue: T,\n guard: (value: unknown) => value is T\n ): T {\n const cached = this.cache.get(flagKey);\n if (cached === undefined) {\n warn(`get: flag \"${flagKey}\" not in cache; using defaultValue`);\n return defaultValue;\n }\n if (!guard(cached)) {\n warn(`get: flag \"${flagKey}\" has wrong type in cache; using defaultValue`);\n return defaultValue;\n }\n return cached;\n }\n\n private startBackgroundRefresh(): void {\n this.destroy();\n this.refreshTimer = setInterval(() => {\n void this.refresh();\n }, this.ttl);\n }\n\n private async fetchAndCache(context?: EvaluationContext): Promise<boolean> {\n if (this.declarations.length === 0) return true;\n\n const body = this.declarations.map((d) => ({\n flagKey: d.flagKey,\n type: d.type,\n defaultValue: d.defaultValue,\n ...(context ? { context } : {}),\n }));\n\n try {\n const response = await fetch(`${this.proxyUrl}/evaluate/batch`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-Flags-Api-Key': this.apiKey,\n },\n body: JSON.stringify(body),\n });\n\n if (!response.ok) {\n return false;\n }\n\n const results = (await response.json()) as EvaluationResult[];\n if (!Array.isArray(results)) {\n return false;\n }\n\n const values: Record<string, unknown> = {};\n for (const result of results) {\n if (result && typeof result.flagKey === 'string') {\n values[result.flagKey] = result.value;\n }\n }\n this.cache.set(values);\n return true;\n } catch {\n return false;\n }\n }\n}\n","import { FlagsClientImpl } from './client';\nimport type { FlagsClient, FlagsClientOptions } from './types';\n\nexport function createFlagsClient(options: FlagsClientOptions): FlagsClient {\n return new FlagsClientImpl(options);\n}\n\nexport type {\n CachedFlagStore,\n EvaluationContext,\n EvaluationRequest,\n EvaluationResult,\n FlagDeclaration,\n FlagType,\n FlagsClient,\n FlagsClientOptions,\n} from './types';\n\nexport { FlagsCache } from './cache';\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "org.inovus.flags",
3
+ "version": "1.0.0",
4
+ "description": "TypeScript client for cf-flags-proxy with stale-while-revalidate caching",
5
+ "license": "SEE LICENSE IN LICENSE",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "engines": {
9
+ "node": ">=18"
10
+ },
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "main": "./dist/index.cjs",
15
+ "module": "./dist/index.js",
16
+ "types": "./dist/index.d.ts",
17
+ "exports": {
18
+ ".": {
19
+ "types": "./dist/index.d.ts",
20
+ "import": "./dist/index.js",
21
+ "require": "./dist/index.cjs"
22
+ }
23
+ },
24
+ "files": [
25
+ "dist",
26
+ "LICENSE",
27
+ "README.md"
28
+ ],
29
+ "scripts": {
30
+ "build": "tsup",
31
+ "prepublishOnly": "npm run build",
32
+ "test": "vitest run --coverage",
33
+ "typecheck": "tsc --noEmit"
34
+ },
35
+ "devDependencies": {
36
+ "@vitest/coverage-v8": "^3.0.9",
37
+ "happy-dom": "^17.4.4",
38
+ "tsup": "^8.4.0",
39
+ "typescript": "^5.8.2",
40
+ "vitest": "^3.0.9"
41
+ }
42
+ }