@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,3 @@
1
+ export { createLoggingHook, createStaticContextHook } from "./hooks.js";
2
+ export { DEFAULT_TARGETING_KEY_ATTRIBUTE, createDefaultOpenFeatureContextMapper, mergeFeatureFlagEvaluationContexts, } from "./context.js";
3
+ export { createOpenFeatureWebProvider, MergedOpenFeatureWebProvider, } from "./web-provider.js";
@@ -0,0 +1,39 @@
1
+ import type { FeatureFlagClientConfig, FeatureFlagRuntimeStatus, FeatureFlagSnapshotStore, PersistedFeatureFlagSnapshot } from "./types.js";
2
+ type ScopeParts = {
3
+ scopeKey: string;
4
+ organizationId: string;
5
+ environmentId: string;
6
+ teamId: string | null;
7
+ clientKeyFingerprint: string;
8
+ contextHash: string;
9
+ };
10
+ export declare function serializeCanonicalValue(value: unknown): string;
11
+ export declare function createDefaultFeatureFlagRuntimeStatus(): FeatureFlagRuntimeStatus;
12
+ export declare function buildSnapshotScopeParts<TFlags extends object = object>(params: {
13
+ config: FeatureFlagClientConfig<TFlags>;
14
+ keyPrefix?: string;
15
+ }): Promise<ScopeParts>;
16
+ export declare function createPersistedFeatureFlagSnapshot(params: {
17
+ scope: ScopeParts;
18
+ token: string;
19
+ publicKeyPem: string;
20
+ fetchedAt: string;
21
+ tokenExpiresAt: string | null;
22
+ }): PersistedFeatureFlagSnapshot;
23
+ export declare function doesSnapshotMatchScope(params: {
24
+ snapshot: PersistedFeatureFlagSnapshot;
25
+ scope: ScopeParts;
26
+ }): boolean;
27
+ export declare function isBrowserPersistenceAvailable(): boolean;
28
+ export declare function createLocalStorageFeatureFlagSnapshotStore(): FeatureFlagSnapshotStore;
29
+ export declare function createFileFeatureFlagSnapshotStore(params?: {
30
+ rootDirectory?: string;
31
+ }): FeatureFlagSnapshotStore;
32
+ export declare function resolveSnapshotStore<TFlags extends object = object>(params: {
33
+ config: FeatureFlagClientConfig<TFlags>;
34
+ }): Promise<FeatureFlagSnapshotStore | null>;
35
+ export declare function getSnapshotKeyPrefix<TFlags extends object = object>(params: {
36
+ config: FeatureFlagClientConfig<TFlags>;
37
+ }): string;
38
+ export declare function decodeJwtPayload(token: string): Record<string, unknown>;
39
+ export {};
@@ -0,0 +1,203 @@
1
+ const DEFAULT_SNAPSHOT_KEY_PREFIX = "merged-feature-flags";
2
+ const SNAPSHOT_SCHEMA_VERSION = 1;
3
+ const textEncoder = new TextEncoder();
4
+ const textDecoder = new TextDecoder();
5
+ let nodeModulePromise = null;
6
+ async function getNodeModules() {
7
+ if (!nodeModulePromise) {
8
+ nodeModulePromise = Promise.all([
9
+ import("node:crypto"),
10
+ import("node:fs/promises"),
11
+ import("node:os"),
12
+ import("node:path"),
13
+ ]).then(([crypto, fs, os, path]) => ({ crypto, fs, os, path }));
14
+ }
15
+ return nodeModulePromise;
16
+ }
17
+ function normalizeValue(value) {
18
+ if (Array.isArray(value)) {
19
+ return value.map((item) => normalizeValue(item));
20
+ }
21
+ if (value && typeof value === "object") {
22
+ const entries = Object.entries(value)
23
+ .sort(([left], [right]) => left.localeCompare(right))
24
+ .map(([key, entryValue]) => [key, normalizeValue(entryValue)]);
25
+ return Object.fromEntries(entries);
26
+ }
27
+ return value;
28
+ }
29
+ export function serializeCanonicalValue(value) {
30
+ return JSON.stringify(normalizeValue(value));
31
+ }
32
+ export function createDefaultFeatureFlagRuntimeStatus() {
33
+ return {
34
+ source: "defaults",
35
+ isStale: false,
36
+ lastSuccessfulRefreshAt: null,
37
+ tokenExpiresAt: null,
38
+ lastError: null,
39
+ };
40
+ }
41
+ async function sha256Hex(value) {
42
+ if (globalThis.crypto?.subtle) {
43
+ const digest = await globalThis.crypto.subtle.digest("SHA-256", textEncoder.encode(value));
44
+ return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
45
+ }
46
+ const { crypto } = await getNodeModules();
47
+ return crypto.createHash("sha256").update(value).digest("hex");
48
+ }
49
+ export async function buildSnapshotScopeParts(params) {
50
+ const contextHash = await sha256Hex(serializeCanonicalValue(params.config.evaluationContext ?? null));
51
+ const clientKeyFingerprint = await sha256Hex(params.config.clientKey);
52
+ const prefix = params.keyPrefix?.trim() || DEFAULT_SNAPSHOT_KEY_PREFIX;
53
+ const scopeMaterial = serializeCanonicalValue({
54
+ apiUrl: params.config.apiUrl.replace(/\/$/, ""),
55
+ organizationId: params.config.organizationId.trim(),
56
+ environmentId: params.config.environmentId.trim(),
57
+ teamId: params.config.teamId?.trim() || null,
58
+ clientKeyFingerprint,
59
+ contextHash,
60
+ });
61
+ return {
62
+ scopeKey: `${prefix}:${await sha256Hex(scopeMaterial)}`,
63
+ organizationId: params.config.organizationId.trim(),
64
+ environmentId: params.config.environmentId.trim(),
65
+ teamId: params.config.teamId?.trim() || null,
66
+ clientKeyFingerprint,
67
+ contextHash,
68
+ };
69
+ }
70
+ function buildSnapshotRecord(params) {
71
+ return {
72
+ schemaVersion: SNAPSHOT_SCHEMA_VERSION,
73
+ scopeKey: params.scope.scopeKey,
74
+ organizationId: params.scope.organizationId,
75
+ environmentId: params.scope.environmentId,
76
+ teamId: params.scope.teamId,
77
+ clientKeyFingerprint: params.scope.clientKeyFingerprint,
78
+ contextHash: params.scope.contextHash,
79
+ token: params.token,
80
+ publicKeyPem: params.publicKeyPem,
81
+ fetchedAt: params.fetchedAt,
82
+ tokenExpiresAt: params.tokenExpiresAt,
83
+ };
84
+ }
85
+ export function createPersistedFeatureFlagSnapshot(params) {
86
+ return buildSnapshotRecord(params);
87
+ }
88
+ export function doesSnapshotMatchScope(params) {
89
+ return (params.snapshot.schemaVersion === SNAPSHOT_SCHEMA_VERSION &&
90
+ params.snapshot.scopeKey === params.scope.scopeKey &&
91
+ params.snapshot.organizationId === params.scope.organizationId &&
92
+ params.snapshot.environmentId === params.scope.environmentId &&
93
+ params.snapshot.teamId === params.scope.teamId &&
94
+ params.snapshot.clientKeyFingerprint === params.scope.clientKeyFingerprint &&
95
+ params.snapshot.contextHash === params.scope.contextHash);
96
+ }
97
+ export function isBrowserPersistenceAvailable() {
98
+ return typeof window !== "undefined" && typeof window.localStorage !== "undefined";
99
+ }
100
+ export function createLocalStorageFeatureFlagSnapshotStore() {
101
+ return {
102
+ async load(key) {
103
+ if (!isBrowserPersistenceAvailable()) {
104
+ return null;
105
+ }
106
+ try {
107
+ const raw = window.localStorage.getItem(key);
108
+ return raw ? JSON.parse(raw) : null;
109
+ }
110
+ catch {
111
+ return null;
112
+ }
113
+ },
114
+ async save(key, snapshot) {
115
+ if (!isBrowserPersistenceAvailable()) {
116
+ return;
117
+ }
118
+ try {
119
+ window.localStorage.setItem(key, JSON.stringify(snapshot));
120
+ }
121
+ catch {
122
+ }
123
+ },
124
+ async remove(key) {
125
+ if (!isBrowserPersistenceAvailable()) {
126
+ return;
127
+ }
128
+ try {
129
+ window.localStorage.removeItem(key);
130
+ }
131
+ catch {
132
+ }
133
+ },
134
+ };
135
+ }
136
+ function sanitizeKeyForFilename(key) {
137
+ return encodeURIComponent(key);
138
+ }
139
+ export function createFileFeatureFlagSnapshotStore(params) {
140
+ return {
141
+ async load(key) {
142
+ const { fs, os, path } = await getNodeModules();
143
+ const rootDirectory = params?.rootDirectory ?? path.join(os.tmpdir(), DEFAULT_SNAPSHOT_KEY_PREFIX);
144
+ const filePath = path.join(rootDirectory, `${sanitizeKeyForFilename(key)}.json`);
145
+ try {
146
+ const raw = await fs.readFile(filePath, "utf-8");
147
+ return JSON.parse(raw);
148
+ }
149
+ catch {
150
+ return null;
151
+ }
152
+ },
153
+ async save(key, snapshot) {
154
+ const { fs, os, path } = await getNodeModules();
155
+ const rootDirectory = params?.rootDirectory ?? path.join(os.tmpdir(), DEFAULT_SNAPSHOT_KEY_PREFIX);
156
+ const filePath = path.join(rootDirectory, `${sanitizeKeyForFilename(key)}.json`);
157
+ const tempFilePath = path.join(rootDirectory, `${sanitizeKeyForFilename(key)}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`);
158
+ await fs.mkdir(rootDirectory, { recursive: true });
159
+ await fs.writeFile(tempFilePath, JSON.stringify(snapshot), "utf-8");
160
+ await fs.rename(tempFilePath, filePath);
161
+ },
162
+ async remove(key) {
163
+ const { fs, os, path } = await getNodeModules();
164
+ const rootDirectory = params?.rootDirectory ?? path.join(os.tmpdir(), DEFAULT_SNAPSHOT_KEY_PREFIX);
165
+ const filePath = path.join(rootDirectory, `${sanitizeKeyForFilename(key)}.json`);
166
+ try {
167
+ await fs.rm(filePath, { force: true });
168
+ }
169
+ catch {
170
+ }
171
+ },
172
+ };
173
+ }
174
+ export async function resolveSnapshotStore(params) {
175
+ const persistence = params.config.snapshotPersistence;
176
+ if (persistence === false) {
177
+ return null;
178
+ }
179
+ if (persistence?.store) {
180
+ return persistence.store;
181
+ }
182
+ if (isBrowserPersistenceAvailable()) {
183
+ return createLocalStorageFeatureFlagSnapshotStore();
184
+ }
185
+ return createFileFeatureFlagSnapshotStore();
186
+ }
187
+ export function getSnapshotKeyPrefix(params) {
188
+ const persistence = params.config.snapshotPersistence;
189
+ return persistence?.keyPrefix?.trim() || DEFAULT_SNAPSHOT_KEY_PREFIX;
190
+ }
191
+ export function decodeJwtPayload(token) {
192
+ const segments = token.split(".");
193
+ if (segments.length !== 3) {
194
+ throw new Error("JWT must contain exactly three segments.");
195
+ }
196
+ const payloadSegment = segments[1];
197
+ const normalized = payloadSegment.replace(/-/g, "+").replace(/_/g, "/");
198
+ const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "=");
199
+ const payloadBytes = typeof atob === "function"
200
+ ? Uint8Array.from(atob(padded), (character) => character.charCodeAt(0))
201
+ : Uint8Array.from(Buffer.from(padded, "base64"));
202
+ return JSON.parse(textDecoder.decode(payloadBytes));
203
+ }
@@ -0,0 +1,52 @@
1
+ import type { EvaluatedFlag } from "@repo/types-v2";
2
+ import { type ReactNode } from "react";
3
+ import type { MergedFeatureFlags } from "../client.js";
4
+ import type { FlagRegistry } from "../types.js";
5
+ import { type FeatureFlagClient, type FeatureFlagLoadState } from "./provider.js";
6
+ export declare function resolveFlagFromContext(params: {
7
+ client: Pick<FeatureFlagClient, "getFlag">;
8
+ flags: EvaluatedFlag[];
9
+ idOrName: string;
10
+ }): EvaluatedFlag | undefined;
11
+ export declare function useFeatureFlagStatus(): FeatureFlagLoadState;
12
+ export declare function useUntypedFeatureFlag(idOrName: string): {
13
+ enabled: boolean;
14
+ value: unknown;
15
+ };
16
+ export declare function useUntypedFeatureFlags(): EvaluatedFlag[];
17
+ export declare function useUntypedFeatureFlagClient(): FeatureFlagClient;
18
+ export type FeatureFlagRenderState<TValue> = {
19
+ enabled: boolean;
20
+ value: TValue | undefined;
21
+ status: FeatureFlagLoadState;
22
+ };
23
+ type FeatureFlagMatchValue<TValue> = Extract<TValue, string | number | boolean>;
24
+ export type TypedFeatureFlagProps<TFlags extends FlagRegistry, K extends string & keyof TFlags> = {
25
+ name: K;
26
+ children: ReactNode | ((props: FeatureFlagRenderState<TFlags[K]>) => ReactNode);
27
+ fallback?: ReactNode;
28
+ loading?: ReactNode;
29
+ matchValue?: FeatureFlagMatchValue<TFlags[K]>;
30
+ };
31
+ export type FeatureFlagComponentProps<TFlags extends FlagRegistry> = {
32
+ [K in string & keyof TFlags]: TypedFeatureFlagProps<TFlags, K>;
33
+ }[string & keyof TFlags];
34
+ export type TypedFeatureFlagComponent<TFlags extends FlagRegistry> = <K extends string & keyof TFlags>(props: TypedFeatureFlagProps<TFlags, K>) => ReactNode;
35
+ export declare function createTypedHooks<TFlags extends FlagRegistry>(): {
36
+ FeatureFlagProvider: (props: {
37
+ client: FeatureFlagClient;
38
+ blockUntilReady: boolean;
39
+ loadingFallback?: ReactNode;
40
+ initialFlags?: EvaluatedFlag[];
41
+ children: ReactNode;
42
+ }) => import("react/jsx-runtime").JSX.Element;
43
+ FeatureFlag: TypedFeatureFlagComponent<TFlags>;
44
+ useFeatureFlag: <K extends string & keyof TFlags>(name: K) => {
45
+ enabled: boolean;
46
+ value: TFlags[K] | undefined;
47
+ };
48
+ useFeatureFlags: () => EvaluatedFlag[];
49
+ useFeatureFlagClient: () => MergedFeatureFlags<TFlags>;
50
+ useFeatureFlagStatus: () => FeatureFlagLoadState;
51
+ };
52
+ export {};
@@ -0,0 +1,78 @@
1
+ import { useContext } from "react";
2
+ import { FeatureFlagContext, FeatureFlagProvider, } from "./provider.js";
3
+ function useFeatureFlagContext() {
4
+ const context = useContext(FeatureFlagContext);
5
+ if (!context) {
6
+ throw new Error("Feature flag hooks and components must be used within a <FeatureFlagProvider>.");
7
+ }
8
+ return context;
9
+ }
10
+ export function resolveFlagFromContext(params) {
11
+ return (params.flags.find((flag) => flag.id === params.idOrName || flag.name === params.idOrName) ??
12
+ params.client.getFlag(params.idOrName));
13
+ }
14
+ export function useFeatureFlagStatus() {
15
+ const { loadState } = useFeatureFlagContext();
16
+ return loadState;
17
+ }
18
+ export function useUntypedFeatureFlag(idOrName) {
19
+ const { client, flags } = useFeatureFlagContext();
20
+ const flag = resolveFlagFromContext({ client, flags, idOrName });
21
+ return { enabled: flag?.enabled ?? false, value: flag?.value };
22
+ }
23
+ export function useUntypedFeatureFlags() {
24
+ const { flags } = useFeatureFlagContext();
25
+ return flags;
26
+ }
27
+ export function useUntypedFeatureFlagClient() {
28
+ const { client } = useFeatureFlagContext();
29
+ return client;
30
+ }
31
+ export function createTypedHooks() {
32
+ function TypedFeatureFlagProvider(props) {
33
+ return FeatureFlagProvider(props);
34
+ }
35
+ function typedUseFeatureFlag(name) {
36
+ const { client, flags } = useFeatureFlagContext();
37
+ const flag = resolveFlagFromContext({ client, flags, idOrName: name });
38
+ return {
39
+ enabled: flag?.enabled ?? false,
40
+ value: flag?.value,
41
+ };
42
+ }
43
+ function typedUseFeatureFlags() {
44
+ const { flags } = useFeatureFlagContext();
45
+ return flags;
46
+ }
47
+ function typedUseFeatureFlagClient() {
48
+ const { client } = useFeatureFlagContext();
49
+ return client;
50
+ }
51
+ function typedUseFeatureFlagStatus() {
52
+ return useFeatureFlagStatus();
53
+ }
54
+ const TypedFeatureFlag = (props) => {
55
+ const { children, fallback, loading, matchValue, name } = props;
56
+ const status = typedUseFeatureFlagStatus();
57
+ const { enabled, value } = typedUseFeatureFlag(name);
58
+ if (status.isLoading) {
59
+ return loading ?? fallback ?? null;
60
+ }
61
+ const shouldRenderChildren = typeof matchValue === "undefined" ? enabled : Object.is(value, matchValue);
62
+ if (!shouldRenderChildren) {
63
+ return fallback ?? null;
64
+ }
65
+ if (typeof children === "function") {
66
+ return children({ enabled, value, status });
67
+ }
68
+ return children;
69
+ };
70
+ return {
71
+ FeatureFlagProvider: TypedFeatureFlagProvider,
72
+ FeatureFlag: TypedFeatureFlag,
73
+ useFeatureFlag: typedUseFeatureFlag,
74
+ useFeatureFlags: typedUseFeatureFlags,
75
+ useFeatureFlagClient: typedUseFeatureFlagClient,
76
+ useFeatureFlagStatus: typedUseFeatureFlagStatus,
77
+ };
78
+ }
@@ -0,0 +1,71 @@
1
+ import { type ReactNode } from "react";
2
+ import type { EvaluatedFlag } from "@repo/types-v2";
3
+ import type { FeatureFlagRuntimeStatus } from "../types.js";
4
+ export interface FeatureFlagClient {
5
+ initialize(): Promise<void>;
6
+ destroy(): void;
7
+ isEnabled(name: string): boolean;
8
+ getValue(name: string): unknown;
9
+ getFlag(name: string): EvaluatedFlag | undefined;
10
+ getAllFlags(): EvaluatedFlag[];
11
+ getSnapshot(): EvaluatedFlag[];
12
+ getStatus(): FeatureFlagRuntimeStatus;
13
+ subscribe(onStoreChange: () => void): () => void;
14
+ refresh(): Promise<void>;
15
+ }
16
+ export type FeatureFlagProviderStatus = "idle" | "loading" | "ready" | "error";
17
+ export type FeatureFlagLoadState = {
18
+ status: FeatureFlagProviderStatus;
19
+ isLoading: boolean;
20
+ isReady: boolean;
21
+ error: Error | null;
22
+ source: FeatureFlagRuntimeStatus["source"];
23
+ isStale: boolean;
24
+ lastSuccessfulRefreshAt: string | null;
25
+ tokenExpiresAt: string | null;
26
+ };
27
+ export type FeatureFlagContextValue = {
28
+ client: FeatureFlagClient;
29
+ flags: EvaluatedFlag[];
30
+ loadState: FeatureFlagLoadState;
31
+ };
32
+ export declare const FeatureFlagContext: import("react").Context<FeatureFlagContextValue | null>;
33
+ export type FeatureFlagProviderProps = {
34
+ client: FeatureFlagClient;
35
+ blockUntilReady: boolean;
36
+ loadingFallback?: ReactNode;
37
+ initialFlags?: EvaluatedFlag[];
38
+ children: ReactNode;
39
+ };
40
+ export declare function createFeatureFlagLoadState(params: {
41
+ status: FeatureFlagProviderStatus;
42
+ error: Error | null;
43
+ runtimeStatus: FeatureFlagRuntimeStatus;
44
+ }): FeatureFlagLoadState;
45
+ export declare function shouldRenderFeatureFlagLoadingFallback(params: {
46
+ blockUntilReady: boolean;
47
+ loadState: FeatureFlagLoadState;
48
+ }): boolean;
49
+ export declare function createSnapshotStore(params: {
50
+ client: FeatureFlagClient;
51
+ initialFlags?: EvaluatedFlag[];
52
+ }): {
53
+ subscribe(onStoreChange: () => void): () => void;
54
+ getSnapshot(): {
55
+ id: string;
56
+ name: string;
57
+ type: "BOOLEAN" | "STRING" | "NUMBER" | "JSON" | "ARRAY";
58
+ teamId: string | null;
59
+ enabled: boolean;
60
+ value: unknown;
61
+ }[];
62
+ getServerSnapshot(): {
63
+ id: string;
64
+ name: string;
65
+ type: "BOOLEAN" | "STRING" | "NUMBER" | "JSON" | "ARRAY";
66
+ teamId: string | null;
67
+ enabled: boolean;
68
+ value: unknown;
69
+ }[];
70
+ };
71
+ export declare function FeatureFlagProvider(props: FeatureFlagProviderProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,99 @@
1
+ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useEffect, useMemo, useState, useSyncExternalStore } from "react";
3
+ export const FeatureFlagContext = createContext(null);
4
+ const EMPTY_EVALUATED_FLAGS = [];
5
+ export function createFeatureFlagLoadState(params) {
6
+ return {
7
+ status: params.status,
8
+ isLoading: params.status === "loading",
9
+ isReady: params.status === "ready",
10
+ error: params.error ?? params.runtimeStatus.lastError,
11
+ source: params.runtimeStatus.source,
12
+ isStale: params.runtimeStatus.isStale,
13
+ lastSuccessfulRefreshAt: params.runtimeStatus.lastSuccessfulRefreshAt,
14
+ tokenExpiresAt: params.runtimeStatus.tokenExpiresAt,
15
+ };
16
+ }
17
+ export function shouldRenderFeatureFlagLoadingFallback(params) {
18
+ return params.blockUntilReady && params.loadState.isLoading;
19
+ }
20
+ export function createSnapshotStore(params) {
21
+ let cachedFlags = params.initialFlags ?? params.client.getSnapshot();
22
+ let hasObservedClientUpdate = false;
23
+ function areSnapshotsEqual(left, right) {
24
+ if (left.length !== right.length) {
25
+ return false;
26
+ }
27
+ for (let index = 0; index < left.length; index++) {
28
+ const leftFlag = left[index];
29
+ const rightFlag = right[index];
30
+ if (leftFlag?.id !== rightFlag?.id ||
31
+ leftFlag?.name !== rightFlag?.name ||
32
+ leftFlag?.type !== rightFlag?.type ||
33
+ leftFlag?.teamId !== rightFlag?.teamId ||
34
+ leftFlag?.enabled !== rightFlag?.enabled ||
35
+ JSON.stringify(leftFlag?.value) !== JSON.stringify(rightFlag?.value)) {
36
+ return false;
37
+ }
38
+ }
39
+ return true;
40
+ }
41
+ return {
42
+ subscribe(onStoreChange) {
43
+ return params.client.subscribe(() => {
44
+ hasObservedClientUpdate = true;
45
+ onStoreChange();
46
+ });
47
+ },
48
+ getSnapshot() {
49
+ const current = params.client.getSnapshot();
50
+ const shouldAdoptClientSnapshot = current.length > 0 || !params.initialFlags || hasObservedClientUpdate;
51
+ if (shouldAdoptClientSnapshot && !areSnapshotsEqual(current, cachedFlags)) {
52
+ cachedFlags = current;
53
+ }
54
+ return cachedFlags;
55
+ },
56
+ getServerSnapshot() {
57
+ return params.initialFlags ?? EMPTY_EVALUATED_FLAGS;
58
+ },
59
+ };
60
+ }
61
+ export function FeatureFlagProvider(props) {
62
+ const { client, blockUntilReady, loadingFallback, initialFlags, children } = props;
63
+ const [status, setStatus] = useState(initialFlags ? "ready" : "loading");
64
+ const [error, setError] = useState(null);
65
+ useEffect(() => {
66
+ let isDisposed = false;
67
+ setStatus(initialFlags ? "ready" : "loading");
68
+ setError(null);
69
+ client.initialize()
70
+ .then(() => {
71
+ if (isDisposed) {
72
+ return;
73
+ }
74
+ setStatus("ready");
75
+ })
76
+ .catch((initializationError) => {
77
+ if (isDisposed) {
78
+ return;
79
+ }
80
+ setStatus("error");
81
+ setError(initializationError instanceof Error
82
+ ? initializationError
83
+ : new Error("Unknown feature flag initialization error."));
84
+ });
85
+ return () => {
86
+ isDisposed = true;
87
+ client.destroy();
88
+ };
89
+ }, [client, initialFlags]);
90
+ const snapshotStore = useMemo(() => createSnapshotStore({ client, initialFlags }), [client, initialFlags]);
91
+ const flags = useSyncExternalStore(snapshotStore.subscribe, snapshotStore.getSnapshot, snapshotStore.getServerSnapshot);
92
+ const runtimeStatus = useSyncExternalStore(client.subscribe, client.getStatus.bind(client), client.getStatus.bind(client));
93
+ const loadState = useMemo(() => createFeatureFlagLoadState({ status, error, runtimeStatus }), [error, runtimeStatus, status]);
94
+ const value = useMemo(() => ({ client, flags, loadState }), [client, flags, loadState]);
95
+ if (shouldRenderFeatureFlagLoadingFallback({ blockUntilReady, loadState })) {
96
+ return _jsx(_Fragment, { children: loadingFallback ?? null });
97
+ }
98
+ return _jsx(FeatureFlagContext.Provider, { value: value, children: children });
99
+ }
package/dist/react.cjs ADDED
@@ -0,0 +1 @@
1
+ module.exports = require("./cjs/react.js")
@@ -0,0 +1,2 @@
1
+ export { FeatureFlagProvider, type FeatureFlagProviderProps, type FeatureFlagClient } from "./react/provider.js";
2
+ export { createTypedHooks } from "./react/hooks.js";
package/dist/react.js ADDED
@@ -0,0 +1,2 @@
1
+ export { FeatureFlagProvider } from "./react/provider.js";
2
+ export { createTypedHooks } from "./react/hooks.js";
@@ -0,0 +1,28 @@
1
+ import type { EvaluatedFlag, FeatureFlagEvaluationContext } from "@repo/types-v2";
2
+ import type { FeatureFlagClientConfig } from "./types.js";
3
+ export type RemoteFeatureFlagEvaluatorConfig = Pick<FeatureFlagClientConfig, "apiUrl" | "clientKey" | "organizationId" | "environmentId" | "teamId" | "publicKey">;
4
+ export type RemoteFeatureFlagEvaluationResult = {
5
+ expiresAt: string | null;
6
+ fetchedAt: string;
7
+ flags: EvaluatedFlag[];
8
+ };
9
+ export declare class RemoteFeatureFlagEvaluator {
10
+ private publicKeyPem;
11
+ private publicKeyCrypto;
12
+ private readonly evaluationCache;
13
+ private readonly inflightEvaluations;
14
+ private readonly apiUrl;
15
+ private readonly clientKey;
16
+ private readonly organizationId;
17
+ private readonly environmentId;
18
+ private readonly teamId;
19
+ constructor(config: RemoteFeatureFlagEvaluatorConfig);
20
+ evaluate(context: FeatureFlagEvaluationContext | null | undefined): Promise<RemoteFeatureFlagEvaluationResult>;
21
+ private evaluateUncached;
22
+ private isCachedEvaluationValid;
23
+ private fetchSignedFlags;
24
+ private fetchPublicKey;
25
+ private verifyWithRetry;
26
+ private ensureVerificationKey;
27
+ private fetchWithTimeout;
28
+ }