@mergedapp/feature-flags 0.1.3

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.
Files changed (114) hide show
  1. package/README.md +651 -0
  2. package/dist/cjs/cli/audit.js +117 -0
  3. package/dist/cjs/cli/cleanup.js +105 -0
  4. package/dist/cjs/cli/config-loader.js +102 -0
  5. package/dist/cjs/cli/generate.js +194 -0
  6. package/dist/cjs/cli/parse-args.js +18 -0
  7. package/dist/cjs/cli.js +46 -0
  8. package/dist/cjs/client.js +505 -0
  9. package/dist/cjs/errors.js +24 -0
  10. package/dist/cjs/index.js +13 -0
  11. package/dist/cjs/jwt.js +85 -0
  12. package/dist/cjs/nestjs/bindings.js +36 -0
  13. package/dist/cjs/nestjs/constants.js +7 -0
  14. package/dist/cjs/nestjs/context.js +28 -0
  15. package/dist/cjs/nestjs/decorators.js +50 -0
  16. package/dist/cjs/nestjs/errors.js +25 -0
  17. package/dist/cjs/nestjs/evaluator.js +87 -0
  18. package/dist/cjs/nestjs/guard.js +67 -0
  19. package/dist/cjs/nestjs/interceptor.js +56 -0
  20. package/dist/cjs/nestjs/module.js +70 -0
  21. package/dist/cjs/nestjs/service.js +54 -0
  22. package/dist/cjs/nestjs/types.js +2 -0
  23. package/dist/cjs/nestjs.js +26 -0
  24. package/dist/cjs/openfeature/context.js +166 -0
  25. package/dist/cjs/openfeature/hooks.js +31 -0
  26. package/dist/cjs/openfeature/server-provider.js +107 -0
  27. package/dist/cjs/openfeature/server.js +13 -0
  28. package/dist/cjs/openfeature/shared.js +83 -0
  29. package/dist/cjs/openfeature/web-provider.js +156 -0
  30. package/dist/cjs/openfeature/web.js +13 -0
  31. package/dist/cjs/package.json +3 -0
  32. package/dist/cjs/persistence.js +249 -0
  33. package/dist/cjs/react/hooks.js +86 -0
  34. package/dist/cjs/react/provider.js +106 -0
  35. package/dist/cjs/react.js +7 -0
  36. package/dist/cjs/remote-evaluator.js +162 -0
  37. package/dist/cjs/types.js +2 -0
  38. package/dist/cli/audit.d.ts +3 -0
  39. package/dist/cli/audit.js +114 -0
  40. package/dist/cli/cleanup.d.ts +3 -0
  41. package/dist/cli/cleanup.js +102 -0
  42. package/dist/cli/config-loader.d.ts +26 -0
  43. package/dist/cli/config-loader.js +66 -0
  44. package/dist/cli/generate.d.ts +3 -0
  45. package/dist/cli/generate.js +191 -0
  46. package/dist/cli/parse-args.d.ts +1 -0
  47. package/dist/cli/parse-args.js +15 -0
  48. package/dist/cli.d.ts +1 -0
  49. package/dist/cli.js +45 -0
  50. package/dist/client.d.ts +67 -0
  51. package/dist/client.js +501 -0
  52. package/dist/errors.d.ts +15 -0
  53. package/dist/errors.js +18 -0
  54. package/dist/index.cjs +1 -0
  55. package/dist/index.d.ts +4 -0
  56. package/dist/index.js +3 -0
  57. package/dist/jwt.d.ts +20 -0
  58. package/dist/jwt.js +78 -0
  59. package/dist/nestjs/bindings.d.ts +5 -0
  60. package/dist/nestjs/bindings.js +33 -0
  61. package/dist/nestjs/constants.d.ts +4 -0
  62. package/dist/nestjs/constants.js +4 -0
  63. package/dist/nestjs/context.d.ts +12 -0
  64. package/dist/nestjs/context.js +24 -0
  65. package/dist/nestjs/decorators.d.ts +4 -0
  66. package/dist/nestjs/decorators.js +45 -0
  67. package/dist/nestjs/errors.d.ts +12 -0
  68. package/dist/nestjs/errors.js +20 -0
  69. package/dist/nestjs/evaluator.d.ts +17 -0
  70. package/dist/nestjs/evaluator.js +83 -0
  71. package/dist/nestjs/guard.d.ts +19 -0
  72. package/dist/nestjs/guard.js +63 -0
  73. package/dist/nestjs/interceptor.d.ts +10 -0
  74. package/dist/nestjs/interceptor.js +53 -0
  75. package/dist/nestjs/module.d.ts +6 -0
  76. package/dist/nestjs/module.js +67 -0
  77. package/dist/nestjs/service.d.ts +30 -0
  78. package/dist/nestjs/service.js +51 -0
  79. package/dist/nestjs/types.d.ts +100 -0
  80. package/dist/nestjs/types.js +1 -0
  81. package/dist/nestjs.cjs +1 -0
  82. package/dist/nestjs.d.ts +10 -0
  83. package/dist/nestjs.js +9 -0
  84. package/dist/openfeature/context.d.ts +10 -0
  85. package/dist/openfeature/context.js +160 -0
  86. package/dist/openfeature/hooks.d.ts +6 -0
  87. package/dist/openfeature/hooks.js +27 -0
  88. package/dist/openfeature/server-provider.d.ts +20 -0
  89. package/dist/openfeature/server-provider.js +102 -0
  90. package/dist/openfeature/server.cjs +1 -0
  91. package/dist/openfeature/server.d.ts +3 -0
  92. package/dist/openfeature/server.js +3 -0
  93. package/dist/openfeature/shared.d.ts +37 -0
  94. package/dist/openfeature/shared.js +74 -0
  95. package/dist/openfeature/web-provider.d.ts +27 -0
  96. package/dist/openfeature/web-provider.js +151 -0
  97. package/dist/openfeature/web.cjs +1 -0
  98. package/dist/openfeature/web.d.ts +3 -0
  99. package/dist/openfeature/web.js +3 -0
  100. package/dist/persistence.d.ts +39 -0
  101. package/dist/persistence.js +203 -0
  102. package/dist/react/hooks.d.ts +52 -0
  103. package/dist/react/hooks.js +78 -0
  104. package/dist/react/provider.d.ts +71 -0
  105. package/dist/react/provider.js +99 -0
  106. package/dist/react.cjs +1 -0
  107. package/dist/react.d.ts +2 -0
  108. package/dist/react.js +2 -0
  109. package/dist/remote-evaluator.d.ts +28 -0
  110. package/dist/remote-evaluator.js +158 -0
  111. package/dist/types.d.ts +56 -0
  112. package/dist/types.js +1 -0
  113. package/featureflags.config.schema.json +38 -0
  114. package/package.json +107 -0
@@ -0,0 +1,505 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MergedFeatureFlags = void 0;
4
+ const errors_js_1 = require("./errors.js");
5
+ const persistence_js_1 = require("./persistence.js");
6
+ const jwt_js_1 = require("./jwt.js");
7
+ const DEFAULT_REFRESH_INTERVAL_MS = 60_000;
8
+ const BACKOFF_BASE_MS = 5_000;
9
+ const BACKOFF_CAP_MS = 300_000;
10
+ class MergedFeatureFlags {
11
+ flagsById = new Map();
12
+ flagsByName = new Map();
13
+ publicKeyPem;
14
+ publicKeyCrypto = null;
15
+ refreshTimer = null;
16
+ backoffMs = 0;
17
+ consecutiveFailures = 0;
18
+ listeners = new Set();
19
+ storeListeners = new Set();
20
+ initialized = false;
21
+ visibilityHandler = null;
22
+ pendingScopeTransition = false;
23
+ runtimeStatus = (0, persistence_js_1.createDefaultFeatureFlagRuntimeStatus)();
24
+ snapshotStorePromise = null;
25
+ keyPrefix;
26
+ contextSignature;
27
+ flagIds;
28
+ clientKey;
29
+ apiUrl;
30
+ organizationId;
31
+ environmentId;
32
+ teamId;
33
+ refreshInterval;
34
+ onError;
35
+ onFlagsChanged;
36
+ snapshotPersistence;
37
+ evaluationContext;
38
+ constructor(config) {
39
+ this.flagIds = config.flagIds ?? null;
40
+ this.clientKey = config.clientKey;
41
+ this.apiUrl = config.apiUrl.replace(/\/$/, "");
42
+ this.organizationId = config.organizationId.trim();
43
+ this.environmentId = config.environmentId.trim();
44
+ this.teamId = config.teamId?.trim() || null;
45
+ this.publicKeyPem = config.publicKey ?? null;
46
+ this.refreshInterval = config.refreshInterval ?? DEFAULT_REFRESH_INTERVAL_MS;
47
+ this.onError = config.onError ?? null;
48
+ this.onFlagsChanged = config.onFlagsChanged ?? null;
49
+ this.snapshotPersistence = config.snapshotPersistence;
50
+ this.evaluationContext = config.evaluationContext ?? null;
51
+ this.contextSignature = (0, persistence_js_1.serializeCanonicalValue)(this.evaluationContext ?? null);
52
+ this.keyPrefix = (0, persistence_js_1.getSnapshotKeyPrefix)({ config });
53
+ if (!this.organizationId) {
54
+ throw new TypeError("organizationId is required.");
55
+ }
56
+ if (!this.environmentId) {
57
+ throw new TypeError("environmentId is required.");
58
+ }
59
+ }
60
+ async initialize() {
61
+ if (this.initialized) {
62
+ return;
63
+ }
64
+ this.initialized = true;
65
+ try {
66
+ const restored = await this.restorePersistedSnapshot().catch(() => false);
67
+ try {
68
+ await this.refresh();
69
+ }
70
+ catch {
71
+ if (!restored) {
72
+ this.clearFlags();
73
+ this.setRuntimeStatus({
74
+ source: "defaults",
75
+ tokenExpiresAt: null,
76
+ });
77
+ this.emitChange({ flagsChanged: true, storeChanged: true });
78
+ }
79
+ }
80
+ }
81
+ finally {
82
+ if (this.refreshInterval > 0) {
83
+ this.startPolling();
84
+ }
85
+ }
86
+ }
87
+ isEnabled(name) {
88
+ return this.resolveFlag(name)?.enabled ?? false;
89
+ }
90
+ getValue(name) {
91
+ const flag = this.resolveFlag(name);
92
+ return flag?.value;
93
+ }
94
+ getFlag(name) {
95
+ return this.resolveFlag(name);
96
+ }
97
+ getAllFlags() {
98
+ return Array.from(this.flagsById.values());
99
+ }
100
+ getStatus() {
101
+ const tokenExpiresAt = this.runtimeStatus.tokenExpiresAt;
102
+ const isStale = tokenExpiresAt != null &&
103
+ Number.isFinite(new Date(tokenExpiresAt).getTime()) &&
104
+ new Date(tokenExpiresAt).getTime() <= Date.now();
105
+ return {
106
+ ...this.runtimeStatus,
107
+ isStale,
108
+ };
109
+ }
110
+ getStatusSnapshot() {
111
+ return this.getStatus();
112
+ }
113
+ async refresh() {
114
+ const restoredForCurrentScope = this.pendingScopeTransition
115
+ ? await this.restorePersistedSnapshot().catch(() => false)
116
+ : false;
117
+ try {
118
+ await this.ensureVerificationKey();
119
+ const response = await this.fetchSignedFlags();
120
+ const verifiedFlags = await this.verifyWithRetry(response.token);
121
+ const fetchedAt = new Date().toISOString();
122
+ const flagsChanged = this.updateFlags(verifiedFlags.flags);
123
+ this.resetBackoff();
124
+ this.pendingScopeTransition = false;
125
+ this.setRuntimeStatus({
126
+ source: "network",
127
+ tokenExpiresAt: verifiedFlags.expiresAt,
128
+ lastSuccessfulRefreshAt: fetchedAt,
129
+ lastError: null,
130
+ });
131
+ await this.persistSnapshot({
132
+ fetchedAt,
133
+ token: response.token,
134
+ tokenExpiresAt: verifiedFlags.expiresAt,
135
+ });
136
+ this.emitChange({ flagsChanged, storeChanged: true });
137
+ }
138
+ catch (error) {
139
+ this.incrementBackoff();
140
+ const wrappedError = error instanceof Error ? error : new errors_js_1.FeatureFlagNetworkError("Unknown error during refresh.");
141
+ if (this.pendingScopeTransition && !restoredForCurrentScope) {
142
+ const hadFlags = this.flagsById.size > 0;
143
+ this.clearFlags();
144
+ this.pendingScopeTransition = false;
145
+ this.setRuntimeStatus({
146
+ source: "defaults",
147
+ tokenExpiresAt: null,
148
+ lastSuccessfulRefreshAt: null,
149
+ lastError: wrappedError,
150
+ });
151
+ this.emitChange({ flagsChanged: hadFlags, storeChanged: true });
152
+ }
153
+ else {
154
+ this.setRuntimeStatus({ lastError: wrappedError });
155
+ this.emitChange({ flagsChanged: false, storeChanged: true });
156
+ }
157
+ this.onError?.(wrappedError);
158
+ throw wrappedError;
159
+ }
160
+ }
161
+ onChange(listener) {
162
+ this.listeners.add(listener);
163
+ return () => {
164
+ this.listeners.delete(listener);
165
+ };
166
+ }
167
+ getSnapshot() {
168
+ return this.getAllFlags();
169
+ }
170
+ setEvaluationContext(context) {
171
+ const nextContext = context ?? null;
172
+ const nextSignature = (0, persistence_js_1.serializeCanonicalValue)(nextContext ?? null);
173
+ if (nextSignature === this.contextSignature) {
174
+ this.evaluationContext = nextContext;
175
+ return;
176
+ }
177
+ this.evaluationContext = nextContext;
178
+ this.contextSignature = nextSignature;
179
+ this.pendingScopeTransition = true;
180
+ }
181
+ subscribe(onStoreChange) {
182
+ this.storeListeners.add(onStoreChange);
183
+ return () => {
184
+ this.storeListeners.delete(onStoreChange);
185
+ };
186
+ }
187
+ destroy() {
188
+ if (this.refreshTimer) {
189
+ clearTimeout(this.refreshTimer);
190
+ this.refreshTimer = null;
191
+ }
192
+ if (this.visibilityHandler && typeof document !== "undefined") {
193
+ document.removeEventListener("visibilitychange", this.visibilityHandler);
194
+ this.visibilityHandler = null;
195
+ }
196
+ this.listeners.clear();
197
+ this.storeListeners.clear();
198
+ this.clearFlags();
199
+ this.pendingScopeTransition = false;
200
+ this.runtimeStatus = (0, persistence_js_1.createDefaultFeatureFlagRuntimeStatus)();
201
+ this.initialized = false;
202
+ }
203
+ resolveFlag(name) {
204
+ if (this.flagIds && name in this.flagIds) {
205
+ const id = this.flagIds[name];
206
+ return this.flagsById.get(id);
207
+ }
208
+ return this.flagsById.get(name) ?? this.flagsByName.get(name);
209
+ }
210
+ clearFlags() {
211
+ this.flagsById.clear();
212
+ this.flagsByName.clear();
213
+ }
214
+ emitChange(params) {
215
+ const allFlags = this.getAllFlags();
216
+ if (params.flagsChanged) {
217
+ this.onFlagsChanged?.(allFlags);
218
+ for (const listener of this.listeners) {
219
+ listener(allFlags);
220
+ }
221
+ }
222
+ if (params.storeChanged) {
223
+ for (const listener of this.storeListeners) {
224
+ listener();
225
+ }
226
+ }
227
+ }
228
+ updateFlags(flags) {
229
+ const oldFlags = new Map(this.flagsById);
230
+ this.clearFlags();
231
+ for (const flag of flags) {
232
+ this.flagsById.set(flag.id, flag);
233
+ this.flagsByName.set(flag.name, flag);
234
+ }
235
+ return this.hasFlagsChanged(oldFlags, this.flagsById);
236
+ }
237
+ hasFlagsChanged(oldFlags, newFlags) {
238
+ if (oldFlags.size !== newFlags.size) {
239
+ return true;
240
+ }
241
+ for (const [id, newFlag] of newFlags) {
242
+ const oldFlag = oldFlags.get(id);
243
+ if (!oldFlag) {
244
+ return true;
245
+ }
246
+ if (oldFlag.enabled !== newFlag.enabled ||
247
+ oldFlag.name !== newFlag.name ||
248
+ JSON.stringify(oldFlag.value) !== JSON.stringify(newFlag.value)) {
249
+ return true;
250
+ }
251
+ }
252
+ return false;
253
+ }
254
+ setRuntimeStatus(patch) {
255
+ this.runtimeStatus = {
256
+ ...this.runtimeStatus,
257
+ ...patch,
258
+ };
259
+ }
260
+ async fetchSignedFlags() {
261
+ const url = new URL("/api/feature-flags/evaluate/signed", this.apiUrl);
262
+ const payload = {
263
+ organizationId: this.organizationId,
264
+ environmentId: this.environmentId,
265
+ ...(this.teamId ? { teamId: this.teamId } : {}),
266
+ ...(this.evaluationContext ? { context: this.evaluationContext } : {}),
267
+ };
268
+ let response;
269
+ try {
270
+ response = await fetch(url, {
271
+ method: "POST",
272
+ headers: {
273
+ Authorization: `Bearer ${this.clientKey}`,
274
+ "Content-Type": "application/json",
275
+ Accept: "application/json",
276
+ },
277
+ body: JSON.stringify(payload),
278
+ });
279
+ }
280
+ catch (error) {
281
+ throw new errors_js_1.FeatureFlagNetworkError("Failed to fetch feature flags.", { cause: error });
282
+ }
283
+ if (!response.ok) {
284
+ throw new errors_js_1.FeatureFlagNetworkError(`Feature flag API returned ${response.status}: ${response.statusText}`);
285
+ }
286
+ return (await response.json());
287
+ }
288
+ async fetchPublicKey() {
289
+ const url = `${this.apiUrl}/api/feature-flags/public-key`;
290
+ let response;
291
+ try {
292
+ response = await fetch(url);
293
+ }
294
+ catch (error) {
295
+ throw new errors_js_1.FeatureFlagNetworkError("Failed to fetch public key.", { cause: error });
296
+ }
297
+ if (!response.ok) {
298
+ throw new errors_js_1.FeatureFlagNetworkError(`Public key endpoint returned ${response.status}: ${response.statusText}`);
299
+ }
300
+ const body = (await response.json());
301
+ return body.publicKey;
302
+ }
303
+ async verifyWithRetry(token) {
304
+ try {
305
+ return await (0, jwt_js_1.verifyFeatureFlagTokenDetailed)({ token, publicKey: this.publicKeyCrypto });
306
+ }
307
+ catch (error) {
308
+ if (!(error instanceof errors_js_1.FeatureFlagVerificationError)) {
309
+ throw error;
310
+ }
311
+ (0, jwt_js_1.resetPublicKeyCache)();
312
+ this.publicKeyPem = await this.fetchPublicKey();
313
+ this.publicKeyCrypto = await (0, jwt_js_1.importPublicKey)(this.publicKeyPem);
314
+ return (0, jwt_js_1.verifyFeatureFlagTokenDetailed)({ token, publicKey: this.publicKeyCrypto });
315
+ }
316
+ }
317
+ async ensureVerificationKey() {
318
+ if (!this.publicKeyPem) {
319
+ this.publicKeyPem = await this.fetchPublicKey();
320
+ }
321
+ if (!this.publicKeyCrypto) {
322
+ this.publicKeyCrypto = await (0, jwt_js_1.importPublicKey)(this.publicKeyPem);
323
+ }
324
+ }
325
+ async getTrustedSnapshotVerificationKey() {
326
+ if (this.publicKeyPem) {
327
+ if (!this.publicKeyCrypto) {
328
+ this.publicKeyCrypto = await (0, jwt_js_1.importPublicKey)(this.publicKeyPem);
329
+ }
330
+ return {
331
+ publicKeyCrypto: this.publicKeyCrypto,
332
+ publicKeyPem: this.publicKeyPem,
333
+ };
334
+ }
335
+ try {
336
+ await this.ensureVerificationKey();
337
+ }
338
+ catch (error) {
339
+ if (error instanceof errors_js_1.FeatureFlagNetworkError) {
340
+ return null;
341
+ }
342
+ throw error;
343
+ }
344
+ if (!this.publicKeyPem || !this.publicKeyCrypto) {
345
+ return null;
346
+ }
347
+ return {
348
+ publicKeyCrypto: this.publicKeyCrypto,
349
+ publicKeyPem: this.publicKeyPem,
350
+ };
351
+ }
352
+ async getSnapshotStore() {
353
+ if (!this.snapshotStorePromise) {
354
+ let snapshotPersistence;
355
+ if (this.snapshotPersistence === undefined) {
356
+ snapshotPersistence = { keyPrefix: this.keyPrefix };
357
+ }
358
+ else if (this.snapshotPersistence === false) {
359
+ snapshotPersistence = false;
360
+ }
361
+ else {
362
+ snapshotPersistence = {
363
+ ...this.snapshotPersistence,
364
+ keyPrefix: this.snapshotPersistence.keyPrefix ?? this.keyPrefix,
365
+ };
366
+ }
367
+ this.snapshotStorePromise = (0, persistence_js_1.resolveSnapshotStore)({
368
+ config: {
369
+ apiUrl: this.apiUrl,
370
+ clientKey: this.clientKey,
371
+ environmentId: this.environmentId,
372
+ organizationId: this.organizationId,
373
+ teamId: this.teamId ?? undefined,
374
+ evaluationContext: this.evaluationContext ?? undefined,
375
+ snapshotPersistence,
376
+ },
377
+ });
378
+ }
379
+ return this.snapshotStorePromise;
380
+ }
381
+ async getScopeParts() {
382
+ return (0, persistence_js_1.buildSnapshotScopeParts)({
383
+ config: {
384
+ apiUrl: this.apiUrl,
385
+ clientKey: this.clientKey,
386
+ environmentId: this.environmentId,
387
+ organizationId: this.organizationId,
388
+ teamId: this.teamId ?? undefined,
389
+ evaluationContext: this.evaluationContext ?? undefined,
390
+ },
391
+ keyPrefix: this.keyPrefix,
392
+ });
393
+ }
394
+ async restorePersistedSnapshot() {
395
+ const store = await this.getSnapshotStore();
396
+ if (!store) {
397
+ return false;
398
+ }
399
+ const scope = await this.getScopeParts();
400
+ const snapshot = await store.load(scope.scopeKey);
401
+ if (!snapshot) {
402
+ return false;
403
+ }
404
+ if (!(0, persistence_js_1.doesSnapshotMatchScope)({ snapshot, scope })) {
405
+ await store.remove(scope.scopeKey);
406
+ return false;
407
+ }
408
+ const trustedKey = await this.getTrustedSnapshotVerificationKey();
409
+ if (!trustedKey) {
410
+ return false;
411
+ }
412
+ try {
413
+ const verified = await (0, jwt_js_1.verifyFeatureFlagTokenDetailed)({
414
+ token: snapshot.token,
415
+ publicKey: trustedKey.publicKeyCrypto,
416
+ allowExpired: true,
417
+ });
418
+ this.publicKeyPem = trustedKey.publicKeyPem;
419
+ this.publicKeyCrypto = trustedKey.publicKeyCrypto;
420
+ this.pendingScopeTransition = false;
421
+ const flagsChanged = this.updateFlags(verified.flags);
422
+ this.setRuntimeStatus({
423
+ source: "persisted",
424
+ tokenExpiresAt: verified.expiresAt,
425
+ lastSuccessfulRefreshAt: snapshot.fetchedAt,
426
+ lastError: null,
427
+ });
428
+ this.emitChange({ flagsChanged, storeChanged: true });
429
+ return true;
430
+ }
431
+ catch {
432
+ await store.remove(scope.scopeKey);
433
+ return false;
434
+ }
435
+ }
436
+ async persistSnapshot(params) {
437
+ const store = await this.getSnapshotStore();
438
+ if (!store || !this.publicKeyPem) {
439
+ return;
440
+ }
441
+ const scope = await this.getScopeParts();
442
+ const snapshot = (0, persistence_js_1.createPersistedFeatureFlagSnapshot)({
443
+ scope,
444
+ token: params.token,
445
+ publicKeyPem: this.publicKeyPem,
446
+ fetchedAt: params.fetchedAt,
447
+ tokenExpiresAt: params.tokenExpiresAt,
448
+ });
449
+ try {
450
+ await store.save(scope.scopeKey, snapshot);
451
+ }
452
+ catch (error) {
453
+ const wrappedError = error instanceof Error ? error : new Error("Failed to persist feature flag snapshot.");
454
+ this.onError?.(wrappedError);
455
+ }
456
+ }
457
+ startPolling() {
458
+ this.scheduleNextPoll();
459
+ if (typeof document !== "undefined") {
460
+ this.visibilityHandler = () => {
461
+ if (document.visibilityState === "hidden") {
462
+ if (this.refreshTimer) {
463
+ clearTimeout(this.refreshTimer);
464
+ this.refreshTimer = null;
465
+ }
466
+ }
467
+ else if (document.visibilityState === "visible") {
468
+ void this.safeRefresh();
469
+ }
470
+ };
471
+ document.addEventListener("visibilitychange", this.visibilityHandler);
472
+ }
473
+ }
474
+ scheduleNextPoll() {
475
+ if (this.refreshTimer) {
476
+ clearTimeout(this.refreshTimer);
477
+ }
478
+ const delay = this.backoffMs > 0 ? this.backoffMs : this.refreshInterval;
479
+ this.refreshTimer = setTimeout(() => {
480
+ void this.safeRefresh();
481
+ }, delay);
482
+ }
483
+ async safeRefresh() {
484
+ try {
485
+ await this.refresh();
486
+ }
487
+ catch {
488
+ }
489
+ finally {
490
+ const isPageVisible = typeof document === "undefined" || document.visibilityState !== "hidden";
491
+ if (this.initialized && this.refreshInterval > 0 && isPageVisible) {
492
+ this.scheduleNextPoll();
493
+ }
494
+ }
495
+ }
496
+ resetBackoff() {
497
+ this.backoffMs = 0;
498
+ this.consecutiveFailures = 0;
499
+ }
500
+ incrementBackoff() {
501
+ this.consecutiveFailures++;
502
+ this.backoffMs = Math.min(BACKOFF_BASE_MS * 2 ** (this.consecutiveFailures - 1), BACKOFF_CAP_MS);
503
+ }
504
+ }
505
+ exports.MergedFeatureFlags = MergedFeatureFlags;
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FeatureFlagVerificationError = exports.FeatureFlagNetworkError = exports.FeatureFlagError = void 0;
4
+ class FeatureFlagError extends Error {
5
+ constructor(message, options) {
6
+ super(message, options);
7
+ this.name = "FeatureFlagError";
8
+ }
9
+ }
10
+ exports.FeatureFlagError = FeatureFlagError;
11
+ class FeatureFlagNetworkError extends FeatureFlagError {
12
+ constructor(message, options) {
13
+ super(message, options);
14
+ this.name = "FeatureFlagNetworkError";
15
+ }
16
+ }
17
+ exports.FeatureFlagNetworkError = FeatureFlagNetworkError;
18
+ class FeatureFlagVerificationError extends FeatureFlagError {
19
+ constructor(message, options) {
20
+ super(message, options);
21
+ this.name = "FeatureFlagVerificationError";
22
+ }
23
+ }
24
+ exports.FeatureFlagVerificationError = FeatureFlagVerificationError;
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createLocalStorageFeatureFlagSnapshotStore = exports.createFileFeatureFlagSnapshotStore = exports.createDefaultFeatureFlagRuntimeStatus = exports.FeatureFlagVerificationError = exports.FeatureFlagNetworkError = exports.FeatureFlagError = exports.MergedFeatureFlags = void 0;
4
+ var client_js_1 = require("./client.js");
5
+ Object.defineProperty(exports, "MergedFeatureFlags", { enumerable: true, get: function () { return client_js_1.MergedFeatureFlags; } });
6
+ var errors_js_1 = require("./errors.js");
7
+ Object.defineProperty(exports, "FeatureFlagError", { enumerable: true, get: function () { return errors_js_1.FeatureFlagError; } });
8
+ Object.defineProperty(exports, "FeatureFlagNetworkError", { enumerable: true, get: function () { return errors_js_1.FeatureFlagNetworkError; } });
9
+ Object.defineProperty(exports, "FeatureFlagVerificationError", { enumerable: true, get: function () { return errors_js_1.FeatureFlagVerificationError; } });
10
+ var persistence_js_1 = require("./persistence.js");
11
+ Object.defineProperty(exports, "createDefaultFeatureFlagRuntimeStatus", { enumerable: true, get: function () { return persistence_js_1.createDefaultFeatureFlagRuntimeStatus; } });
12
+ Object.defineProperty(exports, "createFileFeatureFlagSnapshotStore", { enumerable: true, get: function () { return persistence_js_1.createFileFeatureFlagSnapshotStore; } });
13
+ Object.defineProperty(exports, "createLocalStorageFeatureFlagSnapshotStore", { enumerable: true, get: function () { return persistence_js_1.createLocalStorageFeatureFlagSnapshotStore; } });
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.importPublicKey = importPublicKey;
4
+ exports.resetPublicKeyCache = resetPublicKeyCache;
5
+ exports.verifyFeatureFlagTokenDetailed = verifyFeatureFlagTokenDetailed;
6
+ exports.verifyFeatureFlagToken = verifyFeatureFlagToken;
7
+ exports.verifyFeatureFlagTokenIgnoringExpiration = verifyFeatureFlagTokenIgnoringExpiration;
8
+ const jose_1 = require("jose");
9
+ const errors_js_1 = require("./errors.js");
10
+ const JWT_ALGORITHM = "ES256";
11
+ const JWT_ISSUER = "merged";
12
+ const JWT_TYPE = "feature_flag_values";
13
+ let cachedKey = null;
14
+ let cachedPem = null;
15
+ async function importPublicKey(pem) {
16
+ if (cachedPem === pem && cachedKey) {
17
+ return cachedKey;
18
+ }
19
+ try {
20
+ cachedKey = await (0, jose_1.importSPKI)(pem, JWT_ALGORITHM);
21
+ cachedPem = pem;
22
+ return cachedKey;
23
+ }
24
+ catch (error) {
25
+ cachedKey = null;
26
+ cachedPem = null;
27
+ throw new errors_js_1.FeatureFlagVerificationError("Failed to import public key.", { cause: error });
28
+ }
29
+ }
30
+ function resetPublicKeyCache() {
31
+ cachedKey = null;
32
+ cachedPem = null;
33
+ }
34
+ async function verifyFeatureFlagTokenDetailed(params) {
35
+ try {
36
+ if (!params.allowExpired) {
37
+ const { payload } = await (0, jose_1.jwtVerify)(params.token, params.publicKey, {
38
+ algorithms: [JWT_ALGORITHM],
39
+ issuer: JWT_ISSUER,
40
+ });
41
+ return extractVerifiedTokenPayload(payload);
42
+ }
43
+ const { payload } = await (0, jose_1.compactVerify)(params.token, params.publicKey, {
44
+ algorithms: [JWT_ALGORITHM],
45
+ });
46
+ const rawPayload = JSON.parse(new TextDecoder().decode(payload));
47
+ const notBefore = rawPayload.nbf;
48
+ if (typeof notBefore === "number" && Number.isFinite(notBefore) && Math.floor(Date.now() / 1000) < notBefore) {
49
+ throw new errors_js_1.FeatureFlagVerificationError("JWT payload is not yet valid.");
50
+ }
51
+ return extractVerifiedTokenPayload(rawPayload);
52
+ }
53
+ catch (error) {
54
+ if (error instanceof errors_js_1.FeatureFlagVerificationError) {
55
+ throw error;
56
+ }
57
+ throw new errors_js_1.FeatureFlagVerificationError("JWT verification failed.", { cause: error });
58
+ }
59
+ }
60
+ async function verifyFeatureFlagToken(params) {
61
+ const verified = await verifyFeatureFlagTokenDetailed(params);
62
+ return verified.flags;
63
+ }
64
+ async function verifyFeatureFlagTokenIgnoringExpiration(params) {
65
+ return verifyFeatureFlagTokenDetailed({ ...params, allowExpired: true });
66
+ }
67
+ function extractVerifiedTokenPayload(payload) {
68
+ if (payload.iss !== JWT_ISSUER) {
69
+ throw new errors_js_1.FeatureFlagVerificationError("JWT payload has an unexpected issuer.");
70
+ }
71
+ if (payload.type !== JWT_TYPE) {
72
+ throw new errors_js_1.FeatureFlagVerificationError("JWT payload has an unexpected token type.");
73
+ }
74
+ const flags = payload.flags;
75
+ if (!Array.isArray(flags)) {
76
+ throw new errors_js_1.FeatureFlagVerificationError("JWT payload does not contain a flags array.");
77
+ }
78
+ const expiresAt = typeof payload.exp === "number" && Number.isFinite(payload.exp)
79
+ ? new Date(payload.exp * 1000).toISOString()
80
+ : null;
81
+ return {
82
+ flags: flags,
83
+ expiresAt,
84
+ };
85
+ }
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createTypedNestjsBindings = createTypedNestjsBindings;
4
+ const decorators_js_1 = require("./decorators.js");
5
+ const module_js_1 = require("./module.js");
6
+ function createTypedNestjsBindings(params) {
7
+ return {
8
+ FeatureFlagGate(options) {
9
+ return (0, decorators_js_1.FeatureFlagGate)(options);
10
+ },
11
+ FeatureFlagsModule: {
12
+ forRoot(options) {
13
+ return module_js_1.FeatureFlagsModule.forRoot({
14
+ ...options,
15
+ flagIds: params.flagIds,
16
+ });
17
+ },
18
+ forRootAsync(options) {
19
+ return module_js_1.FeatureFlagsModule.forRootAsync({
20
+ imports: options.imports,
21
+ inject: options.inject,
22
+ async useFactory(...args) {
23
+ const resolvedOptions = await options.useFactory(...args);
24
+ return {
25
+ ...resolvedOptions,
26
+ flagIds: params.flagIds,
27
+ };
28
+ },
29
+ });
30
+ },
31
+ },
32
+ RequireFeatureFlag(options) {
33
+ return (0, decorators_js_1.RequireFeatureFlag)(options);
34
+ },
35
+ };
36
+ }
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.REQUIRE_FEATURE_FLAG_KEY = exports.FEATURE_FLAGS_REQUEST_CONTEXT = exports.FEATURE_FLAGS_EVALUATOR = exports.FEATURE_FLAGS_MODULE_OPTIONS = void 0;
4
+ exports.FEATURE_FLAGS_MODULE_OPTIONS = Symbol("FEATURE_FLAGS_MODULE_OPTIONS");
5
+ exports.FEATURE_FLAGS_EVALUATOR = Symbol("FEATURE_FLAGS_EVALUATOR");
6
+ exports.FEATURE_FLAGS_REQUEST_CONTEXT = Symbol("FEATURE_FLAGS_REQUEST_CONTEXT");
7
+ exports.REQUIRE_FEATURE_FLAG_KEY = "REQUIRE_FEATURE_FLAG";