@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
+ type GenerateArgs = Record<string, string>;
2
+ export declare function generate(args: GenerateArgs): Promise<void>;
3
+ export {};
@@ -0,0 +1,191 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import { basename, dirname, extname, relative, resolve } from "node:path";
3
+ import { FeatureFlagCliConfigLoader } from "./config-loader.js";
4
+ function assertValidCodeKey(codeKey) {
5
+ if (!/^[a-z][A-Za-z0-9]*$/.test(codeKey)) {
6
+ throw new Error(`Invalid feature flag codeKey "${codeKey}". Code keys must be lowerCamelCase alphanumeric identifiers.`);
7
+ }
8
+ }
9
+ function flagTypeToTsType(flagType) {
10
+ switch (flagType) {
11
+ case "BOOLEAN":
12
+ return "boolean";
13
+ case "STRING":
14
+ return "string";
15
+ case "NUMBER":
16
+ return "number";
17
+ case "JSON":
18
+ return "Record<string, unknown>";
19
+ case "ARRAY":
20
+ return "unknown[]";
21
+ default:
22
+ return "unknown";
23
+ }
24
+ }
25
+ function escapeJsDoc(str) {
26
+ return str.replace(/\*\//g, "*\\/").replace(/\n/g, " ");
27
+ }
28
+ async function fetchDefinitions(params) {
29
+ const url = new URL("/api/feature-flags/definitions", params.apiUrl);
30
+ url.searchParams.set("organizationId", params.organizationId);
31
+ if (params.teamId) {
32
+ url.searchParams.set("teamId", params.teamId);
33
+ }
34
+ const response = await fetch(url.toString(), {
35
+ headers: { Authorization: `Bearer ${params.clientKey}` },
36
+ });
37
+ if (!response.ok) {
38
+ throw new Error(`Failed to fetch definitions: ${response.status} ${response.statusText}`);
39
+ }
40
+ const body = (await response.json());
41
+ return body.definitions;
42
+ }
43
+ function generateCode(definitions) {
44
+ const activeFlags = definitions.filter((d) => !d.isArchived);
45
+ if (activeFlags.length === 0) {
46
+ return `// Auto-generated by @mergedapp/feature-flags. Do not edit.
47
+ // Generated at: ${new Date().toISOString()}
48
+ // No active feature flags found.
49
+
50
+ import { MergedFeatureFlags } from "@mergedapp/feature-flags"
51
+ import { createTypedHooks } from "@mergedapp/feature-flags/react"
52
+ import type { FeatureFlagClientConfig } from "@mergedapp/feature-flags"
53
+
54
+ export const FLAGS = {} as const
55
+
56
+ export interface FlagValues {}
57
+
58
+ export type FlagName = keyof typeof FLAGS
59
+
60
+ export function createClient(config: FeatureFlagClientConfig) {
61
+ return new MergedFeatureFlags<FlagValues>({ ...config, flagIds: FLAGS })
62
+ }
63
+
64
+ export const { FeatureFlagProvider, FeatureFlag, useFeatureFlag, useFeatureFlags, useFeatureFlagClient, useFeatureFlagStatus } =
65
+ createTypedHooks<FlagValues>()
66
+ `;
67
+ }
68
+ const flagEntries = activeFlags.map((flag) => {
69
+ assertValidCodeKey(flag.codeKey);
70
+ const doc = flag.description ? ` /** ${escapeJsDoc(flag.description)} */\n` : "";
71
+ return { key: flag.codeKey, id: flag.id, type: flag.type, doc };
72
+ });
73
+ const flagsConst = flagEntries.map((e) => `${e.doc} ${e.key}: "${e.id}",`).join("\n");
74
+ const flagValuesEntries = flagEntries.map((e) => ` ${e.key}: ${flagTypeToTsType(e.type)}`).join("\n");
75
+ return `// Auto-generated by @mergedapp/feature-flags. Do not edit.
76
+ // Generated at: ${new Date().toISOString()}
77
+
78
+ import { MergedFeatureFlags } from "@mergedapp/feature-flags"
79
+ import { createTypedHooks } from "@mergedapp/feature-flags/react"
80
+ import type { FeatureFlagClientConfig } from "@mergedapp/feature-flags"
81
+
82
+ /** Stable flag ID mapping. Names are for readability; IDs are used at runtime. */
83
+ export const FLAGS = {
84
+ ${flagsConst}
85
+ } as const
86
+
87
+ /** Type-safe flag value types inferred from flag definitions. */
88
+ export interface FlagValues {
89
+ ${flagValuesEntries}
90
+ }
91
+
92
+ export type FlagName = keyof typeof FLAGS
93
+
94
+ /** Create a type-safe client bound to this flag registry. */
95
+ export function createClient(config: FeatureFlagClientConfig) {
96
+ return new MergedFeatureFlags<FlagValues>({ ...config, flagIds: FLAGS })
97
+ }
98
+
99
+ /** Pre-typed React hooks, provider, and JSX wrapper bound to this flag registry. */
100
+ export const { FeatureFlagProvider, FeatureFlag, useFeatureFlag, useFeatureFlags, useFeatureFlagClient, useFeatureFlagStatus } =
101
+ createTypedHooks<FlagValues>()
102
+ `;
103
+ }
104
+ function generateNestCode(params) {
105
+ return `// Auto-generated by @mergedapp/feature-flags. Do not edit.
106
+ // Generated at: ${new Date().toISOString()}
107
+
108
+ import {
109
+ createTypedNestjsBindings,
110
+ FeatureFlagContext,
111
+ FeatureFlagsRequestContextStore,
112
+ FeatureFlagsService,
113
+ type FeatureFlagContextFactory,
114
+ type FeatureFlagEvaluationDetails,
115
+ } from "@mergedapp/feature-flags/nestjs"
116
+ import { FLAGS } from "${params.generatedModulePath}"
117
+ import type { FlagValues } from "${params.generatedModulePath}"
118
+
119
+ export {
120
+ FeatureFlagContext,
121
+ FeatureFlagsRequestContextStore,
122
+ FeatureFlagsService,
123
+ }
124
+ export type {
125
+ FeatureFlagContextFactory,
126
+ FeatureFlagEvaluationDetails,
127
+ }
128
+
129
+ export const { FeatureFlagsModule, RequireFeatureFlag, FeatureFlagGate } =
130
+ createTypedNestjsBindings<FlagValues>({ flagIds: FLAGS })
131
+ `;
132
+ }
133
+ function resolveNestOutputPath(outputPath) {
134
+ const extension = extname(outputPath);
135
+ if (extension === ".ts") {
136
+ return outputPath.slice(0, -extension.length) + ".nest.ts";
137
+ }
138
+ return `${outputPath}.nest.ts`;
139
+ }
140
+ function resolveRelativeGeneratedModulePath(params) {
141
+ const mainBaseName = basename(params.mainOutputPath, extname(params.mainOutputPath));
142
+ const nestDirectory = dirname(params.nestOutputPath);
143
+ const mainDirectory = dirname(params.mainOutputPath);
144
+ if (nestDirectory === mainDirectory) {
145
+ return `./${mainBaseName}`;
146
+ }
147
+ const relativeDirectory = relative(nestDirectory, mainDirectory);
148
+ const normalizedDirectory = relativeDirectory.split("\\").join("/");
149
+ return `${normalizedDirectory.startsWith(".") ? normalizedDirectory : `./${normalizedDirectory}`}/${mainBaseName}`;
150
+ }
151
+ export async function generate(args) {
152
+ const loadedConfig = await FeatureFlagCliConfigLoader.load({ configPath: args.config });
153
+ const config = loadedConfig.config;
154
+ const apiUrl = args["api-url"] ?? config.apiUrl ?? process.env.FEATURE_FLAG_API_URL;
155
+ const clientKey = args["client-key"] ?? config.clientKey ?? process.env.FEATURE_FLAG_CLIENT_KEY;
156
+ const outputPath = args.output ??
157
+ FeatureFlagCliConfigLoader.resolveConfigRelativePath({
158
+ value: config.outputPath,
159
+ configPath: loadedConfig.configPath,
160
+ }) ??
161
+ "./src/generated/feature-flags.ts";
162
+ const teamId = args["team-id"] ?? config.teamId;
163
+ const organizationId = args["organization-id"] ?? config.organizationId ?? process.env.FEATURE_FLAG_ORGANIZATION_ID;
164
+ if (!apiUrl) {
165
+ throw new Error("Missing --api-url or FEATURE_FLAG_API_URL env var.");
166
+ }
167
+ if (!clientKey) {
168
+ throw new Error("Missing --client-key or FEATURE_FLAG_CLIENT_KEY env var.");
169
+ }
170
+ if (!organizationId) {
171
+ throw new Error("Missing --organization-id, FEATURE_FLAG_ORGANIZATION_ID, or config.organizationId.");
172
+ }
173
+ console.log(`Fetching flag definitions from ${apiUrl}...`);
174
+ const definitions = await fetchDefinitions({ apiUrl, clientKey, organizationId, teamId });
175
+ console.log(`Found ${definitions.length} flag definitions.`);
176
+ const code = generateCode(definitions);
177
+ const resolvedPath = resolve(outputPath);
178
+ const resolvedNestPath = resolveNestOutputPath(resolvedPath);
179
+ const nestCode = generateNestCode({
180
+ generatedModulePath: resolveRelativeGeneratedModulePath({
181
+ mainOutputPath: resolvedPath,
182
+ nestOutputPath: resolvedNestPath,
183
+ }),
184
+ });
185
+ await mkdir(dirname(resolvedPath), { recursive: true });
186
+ await mkdir(dirname(resolvedNestPath), { recursive: true });
187
+ await writeFile(resolvedPath, code, "utf-8");
188
+ await writeFile(resolvedNestPath, nestCode, "utf-8");
189
+ console.log(`Generated typed flags at ${resolvedPath}`);
190
+ console.log(`Generated typed NestJS bindings at ${resolvedNestPath}`);
191
+ }
@@ -0,0 +1 @@
1
+ export declare function parseArgs(args: string[]): Record<string, string>;
@@ -0,0 +1,15 @@
1
+ export function parseArgs(args) {
2
+ const result = {};
3
+ for (const arg of args) {
4
+ const keyValueMatch = arg.match(/^--([^=]+)=(.+)$/);
5
+ if (keyValueMatch) {
6
+ result[keyValueMatch[1]] = keyValueMatch[2];
7
+ continue;
8
+ }
9
+ const flagMatch = arg.match(/^--(.+)$/);
10
+ if (flagMatch) {
11
+ result[flagMatch[1]] = "true";
12
+ }
13
+ }
14
+ return result;
15
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env node
2
+ import { generate } from "./cli/generate.js";
3
+ import { audit } from "./cli/audit.js";
4
+ import { cleanup } from "./cli/cleanup.js";
5
+ import { parseArgs } from "./cli/parse-args.js";
6
+ const [command, ...args] = process.argv.slice(2);
7
+ async function main() {
8
+ const parsed = parseArgs(args);
9
+ switch (command) {
10
+ case "generate":
11
+ await generate(parsed);
12
+ break;
13
+ case "audit":
14
+ await audit(parsed);
15
+ break;
16
+ case "cleanup":
17
+ await cleanup(parsed);
18
+ break;
19
+ default:
20
+ console.log(`@mergedapp/feature-flags CLI
21
+
22
+ Usage:
23
+ merged-ff generate Generate typed SDK from flag definitions
24
+ merged-ff audit Scan codebase for unused/stale flags
25
+ merged-ff cleanup Remove stale flag references (interactive)
26
+
27
+ Options:
28
+ --api-url=<url> API base URL
29
+ --client-key=<key> API key (or set FEATURE_FLAG_CLIENT_KEY env var)
30
+ --output=<path> Output path for generated file (default: ./src/generated/feature-flags.ts)
31
+ --dir=<path> Directory to scan (audit/cleanup, default: ./src)
32
+ --config=<path> Config file path for generate/audit (default: auto-discover featureflags.config.{json,js,mjs,cjs} from cwd upward)
33
+ --dry-run Preview changes without applying (cleanup)
34
+ --organization-id=<id> Organization ID (required)
35
+ --team-id=<id> Team ID scope`);
36
+ if (command) {
37
+ console.error(`\nUnknown command: ${command}`);
38
+ process.exit(1);
39
+ }
40
+ }
41
+ }
42
+ main().catch((error) => {
43
+ console.error(error instanceof Error ? error.message : error);
44
+ process.exit(1);
45
+ });
@@ -0,0 +1,67 @@
1
+ import type { EvaluatedFlag, FeatureFlagEvaluationContext } from "@repo/types-v2";
2
+ import type { FeatureFlagClientConfig, FeatureFlagRuntimeStatus, FlagRegistry } from "./types.js";
3
+ type ChangeListener = (flags: EvaluatedFlag[]) => void;
4
+ export declare class MergedFeatureFlags<TFlags extends FlagRegistry = FlagRegistry> {
5
+ private flagsById;
6
+ private flagsByName;
7
+ private publicKeyPem;
8
+ private publicKeyCrypto;
9
+ private refreshTimer;
10
+ private backoffMs;
11
+ private consecutiveFailures;
12
+ private listeners;
13
+ private storeListeners;
14
+ private initialized;
15
+ private visibilityHandler;
16
+ private pendingScopeTransition;
17
+ private runtimeStatus;
18
+ private snapshotStorePromise;
19
+ private readonly keyPrefix;
20
+ private contextSignature;
21
+ private readonly flagIds;
22
+ private readonly clientKey;
23
+ private readonly apiUrl;
24
+ private readonly organizationId;
25
+ private readonly environmentId;
26
+ private readonly teamId;
27
+ private readonly refreshInterval;
28
+ private readonly onError;
29
+ private readonly onFlagsChanged;
30
+ private readonly snapshotPersistence;
31
+ private evaluationContext;
32
+ constructor(config: FeatureFlagClientConfig<TFlags>);
33
+ initialize(): Promise<void>;
34
+ isEnabled<K extends string & keyof TFlags>(name: K): boolean;
35
+ getValue<K extends string & keyof TFlags>(name: K): TFlags[K] | undefined;
36
+ getFlag<K extends string & keyof TFlags>(name: K): EvaluatedFlag | undefined;
37
+ getAllFlags(): EvaluatedFlag[];
38
+ getStatus(): FeatureFlagRuntimeStatus;
39
+ getStatusSnapshot(): FeatureFlagRuntimeStatus;
40
+ refresh(): Promise<void>;
41
+ onChange(listener: ChangeListener): () => void;
42
+ getSnapshot(): EvaluatedFlag[];
43
+ setEvaluationContext(context: FeatureFlagEvaluationContext | null | undefined): void;
44
+ subscribe(onStoreChange: () => void): () => void;
45
+ destroy(): void;
46
+ private resolveFlag;
47
+ private clearFlags;
48
+ private emitChange;
49
+ private updateFlags;
50
+ private hasFlagsChanged;
51
+ private setRuntimeStatus;
52
+ private fetchSignedFlags;
53
+ private fetchPublicKey;
54
+ private verifyWithRetry;
55
+ private ensureVerificationKey;
56
+ private getTrustedSnapshotVerificationKey;
57
+ private getSnapshotStore;
58
+ private getScopeParts;
59
+ private restorePersistedSnapshot;
60
+ private persistSnapshot;
61
+ private startPolling;
62
+ private scheduleNextPoll;
63
+ private safeRefresh;
64
+ private resetBackoff;
65
+ private incrementBackoff;
66
+ }
67
+ export {};