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