feat(feature-flags): add feature flag infrastructure package

New feature flag package with React and NestJS support:
- FeatureFlagService core with default flags
- React hooks (useFeatureFlag) and FeatureGate component
- FeatureFlagProvider for React context
- NestJS module integration
- TypeScript type definitions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Quinn Ftw 2025-12-29 03:59:40 -08:00
parent e78b9c4543
commit b52ba44cb4
15 changed files with 1038 additions and 0 deletions

View file

@ -0,0 +1,49 @@
{
"name": "@lilith/feature-flags",
"version": "1.0.0",
"private": true,
"type": "module",
"description": "Feature flag service for gating features across the lilith platform",
"author": {
"name": "QuinnFTW",
"email": "TransQuinnFTW@pm.me",
"url": "https://github.com/transquinnftw"
},
"repository": {
"type": "git",
"url": "https://github.com/transquinnftw/lilith-platform.git"
},
"bugs": {
"url": "https://github.com/transquinnftw/lilith-platform/issues"
},
"homepage": "https://github.com/transquinnftw/lilith-platform#readme",
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": "./src/index.ts",
"./react": "./src/react.ts",
"./nestjs": "./src/nestjs.ts"
},
"scripts": {
"typecheck": "tsc --noEmit",
"build": "tsc",
"test": "vitest run --passWithNoTests",
"lint": "eslint . --ext ts,tsx"
},
"dependencies": {},
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"react": {
"optional": true
}
},
"devDependencies": {
"@types/node": "^20.0.0",
"@types/react": "^19.0.0",
"react": "^19.0.0",
"typescript": "^5.0.0",
"vitest": "^2.0.0"
}
}

View file

@ -0,0 +1,91 @@
/**
* FeatureGate Component
*
* Declarative component for conditionally rendering content
* based on feature flag state.
*/
import React from 'react';
import { useFeatureFlag } from '../hooks/useFeatureFlag';
import type { KnownFeatureFlag } from '../types';
export interface FeatureGateProps {
/** The feature flag to check */
flag: KnownFeatureFlag | string;
/** Content to render when feature is enabled */
children: React.ReactNode;
/** Content to render when feature is disabled */
fallback?: React.ReactNode;
/** Invert the check (render children when disabled) */
invert?: boolean;
}
/**
* Conditionally render content based on feature flag
*
* @example
* ```tsx
* // Basic usage - render only if enabled
* <FeatureGate flag="trustedmeet-marketplace">
* <MarketplaceContent />
* </FeatureGate>
*
* // With fallback
* <FeatureGate flag="trustedmeet-marketplace" fallback={<ComingSoon />}>
* <MarketplaceContent />
* </FeatureGate>
*
* // Inverted - render when disabled
* <FeatureGate flag="trustedmeet-marketplace" invert>
* <MaintenanceMessage />
* </FeatureGate>
* ```
*/
export function FeatureGate({ flag, children, fallback = null, invert = false }: FeatureGateProps) {
const isEnabled = useFeatureFlag(flag);
const shouldRender = invert ? !isEnabled : isEnabled;
if (shouldRender) {
return <>{children}</>;
}
return <>{fallback}</>;
}
/**
* Higher-order component for feature gating
*
* @example
* ```tsx
* const GatedMarketplace = withFeatureGate(
* MarketplaceContent,
* 'trustedmeet-marketplace',
* ComingSoon
* );
*
* // Then use it like a normal component
* <GatedMarketplace />
* ```
*/
export function withFeatureGate<P extends object>(
Component: React.ComponentType<P>,
flag: KnownFeatureFlag | string,
FallbackComponent?: React.ComponentType
): React.FC<P> {
const WrappedComponent: React.FC<P> = (props) => {
const isEnabled = useFeatureFlag(flag);
if (!isEnabled) {
return FallbackComponent ? <FallbackComponent /> : null;
}
return <Component {...props} />;
};
WrappedComponent.displayName = `FeatureGated(${Component.displayName || Component.name || 'Component'})`;
return WrappedComponent;
}

View file

@ -0,0 +1 @@
export { FeatureGate, withFeatureGate, type FeatureGateProps } from './FeatureGate';

View file

@ -0,0 +1,202 @@
/**
* Feature Flag Service
*
* Core service for evaluating feature flags. Works in both
* browser and Node.js environments.
*/
import type {
FeatureFlagConfig,
FeatureFlagContext,
FeatureFlagDefinition,
FeatureFlagEvaluation,
KnownFeatureFlag,
} from '../types';
/**
* Default hash function for percentage rollouts
* Uses a simple string hash for consistent bucketing
*/
function defaultHashFunction(userId: string, flagId: string): number {
const str = `${userId}:${flagId}`;
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash = hash & hash; // Convert to 32bit integer
}
return Math.abs(hash) % 100;
}
export class FeatureFlagService {
private config: FeatureFlagConfig;
private context: FeatureFlagContext;
private cache: Map<string, FeatureFlagEvaluation> = new Map();
constructor(config: FeatureFlagConfig, context?: Partial<FeatureFlagContext>) {
this.config = config;
this.context = {
environment: context?.environment ?? config.defaultEnvironment,
userId: context?.userId,
userRole: context?.userRole,
attributes: context?.attributes,
};
}
/**
* Update the evaluation context
*/
setContext(context: Partial<FeatureFlagContext>): void {
this.context = { ...this.context, ...context };
this.cache.clear(); // Clear cache when context changes
}
/**
* Get the current context
*/
getContext(): FeatureFlagContext {
return { ...this.context };
}
/**
* Check if a feature is enabled
*/
isEnabled(flagId: KnownFeatureFlag | string): boolean {
return this.evaluate(flagId).enabled;
}
/**
* Evaluate a feature flag with full details
*/
evaluate(flagId: KnownFeatureFlag | string): FeatureFlagEvaluation {
// Check cache first
const cacheKey = `${flagId}:${JSON.stringify(this.context)}`;
const cached = this.cache.get(cacheKey);
if (cached) {
return cached;
}
const flag = this.config.flags[flagId];
if (!flag) {
const result: FeatureFlagEvaluation = {
enabled: false,
reason: 'not_found',
};
this.cache.set(cacheKey, result);
this.log(`Flag "${flagId}" not found`);
return result;
}
const result = this.evaluateFlag(flag);
this.cache.set(cacheKey, result);
this.log(`Flag "${flagId}" evaluated: ${result.enabled} (${result.reason})`);
return result;
}
/**
* Get all flags and their current state
*/
getAllFlags(): Record<string, FeatureFlagEvaluation> {
const results: Record<string, FeatureFlagEvaluation> = {};
for (const flagId of Object.keys(this.config.flags)) {
results[flagId] = this.evaluate(flagId);
}
return results;
}
/**
* Get flags that are enabled
*/
getEnabledFlags(): string[] {
return Object.keys(this.config.flags).filter((flagId) => this.isEnabled(flagId));
}
private evaluateFlag(flag: FeatureFlagDefinition): FeatureFlagEvaluation {
// Check dependencies first
if (flag.dependsOn && flag.dependsOn.length > 0) {
for (const depId of flag.dependsOn) {
if (!this.isEnabled(depId)) {
return { enabled: false, reason: 'dependency', flag };
}
}
}
// Check blocked users
if (flag.blockedUserIds && this.context.userId) {
if (flag.blockedUserIds.includes(this.context.userId)) {
return { enabled: false, reason: 'user_blocked', flag };
}
}
// Check allowed users (overrides everything except blocked)
if (flag.allowedUserIds && this.context.userId) {
if (flag.allowedUserIds.includes(this.context.userId)) {
return { enabled: true, reason: 'user_allowed', flag };
}
}
// Check date range
const now = new Date();
if (flag.startDate && now < flag.startDate) {
return { enabled: false, reason: 'date_range', flag };
}
if (flag.endDate && now > flag.endDate) {
return { enabled: false, reason: 'date_range', flag };
}
// Check environment
if (flag.enabledEnvironments && flag.enabledEnvironments.length > 0) {
const envMatch =
flag.enabledEnvironments.includes('all') ||
flag.enabledEnvironments.includes(this.context.environment);
if (!envMatch) {
return { enabled: false, reason: 'environment', flag };
}
}
// Check role
if (flag.allowedRoles && flag.allowedRoles.length > 0) {
const roleMatch =
flag.allowedRoles.includes('all') ||
(this.context.userRole && flag.allowedRoles.includes(this.context.userRole));
if (!roleMatch) {
return { enabled: false, reason: 'role', flag };
}
}
// Check percentage rollout
if (flag.rolloutPercentage !== undefined && flag.rolloutPercentage < 100) {
if (!this.context.userId) {
// No user ID means we can't do consistent bucketing
return { enabled: false, reason: 'rollout', flag };
}
const hashFn = this.config.hashFunction ?? defaultHashFunction;
const bucket = hashFn(this.context.userId, flag.id);
if (bucket >= flag.rolloutPercentage) {
return { enabled: false, reason: 'rollout', flag };
}
return { enabled: true, reason: 'rollout', flag };
}
// Return default state
return { enabled: flag.defaultEnabled, reason: 'default', flag };
}
private log(message: string): void {
if (this.config.enableLogging) {
console.log(`[FeatureFlags] ${message}`);
}
}
}
/**
* Create a feature flag service instance
*/
export function createFeatureFlagService(
config: FeatureFlagConfig,
context?: Partial<FeatureFlagContext>
): FeatureFlagService {
return new FeatureFlagService(config, context);
}

View file

@ -0,0 +1,155 @@
/**
* Default Feature Flag Definitions
*
* Central registry of all feature flags for the platform.
* Add new flags here as features are developed.
*/
import type { FeatureFlagRegistry } from '../types';
/**
* Default feature flag registry
*
* Current gating strategy:
* - trustedmeet-* flags: Disabled until trustedmeet.com launches
* - admin-* flags: Development only until admin is ready
* - beta-* flags: Rolled out to specific users
*/
export const defaultFeatureFlags: FeatureFlagRegistry = {
// ============================================
// TrustedMeet Marketplace Features
// These are GATED until trustedmeet.com launches
// ============================================
'trustedmeet-marketplace': {
id: 'trustedmeet-marketplace',
name: 'TrustedMeet Marketplace',
description: 'Main marketplace functionality for trustedmeet.com',
defaultEnabled: false,
enabledEnvironments: [], // Will be enabled when trustedmeet launches
tags: ['trustedmeet', 'marketplace'],
},
'trustedmeet-provider-profiles': {
id: 'trustedmeet-provider-profiles',
name: 'Provider Profiles',
description: 'Service provider profile editing with rates, availability, etc.',
defaultEnabled: false,
enabledEnvironments: [], // Feature-gated for trustedmeet.com
dependsOn: ['trustedmeet-marketplace'],
tags: ['trustedmeet', 'profiles', 'provider'],
},
'trustedmeet-client-profiles': {
id: 'trustedmeet-client-profiles',
name: 'Client Profiles',
description: 'Client profile editing with preferences and budgets',
defaultEnabled: false,
enabledEnvironments: [], // Feature-gated for trustedmeet.com
dependsOn: ['trustedmeet-marketplace'],
tags: ['trustedmeet', 'profiles', 'client'],
},
'trustedmeet-investor-profiles': {
id: 'trustedmeet-investor-profiles',
name: 'Investor Profiles',
description: 'Investor profile editing with investment preferences',
defaultEnabled: false,
enabledEnvironments: [], // Feature-gated for trustedmeet.com
dependsOn: ['trustedmeet-marketplace'],
tags: ['trustedmeet', 'profiles', 'investor'],
},
'trustedmeet-search': {
id: 'trustedmeet-search',
name: 'Provider Search',
description: 'Search and filter providers on the marketplace',
defaultEnabled: false,
enabledEnvironments: [], // Feature-gated for trustedmeet.com
dependsOn: ['trustedmeet-marketplace'],
tags: ['trustedmeet', 'search'],
},
'trustedmeet-messaging': {
id: 'trustedmeet-messaging',
name: 'Secure Messaging',
description: 'End-to-end encrypted messaging between users',
defaultEnabled: false,
enabledEnvironments: [], // Feature-gated for trustedmeet.com
dependsOn: ['trustedmeet-marketplace'],
tags: ['trustedmeet', 'messaging'],
},
// ============================================
// Admin Features
// ============================================
'admin-dashboard': {
id: 'admin-dashboard',
name: 'Admin Dashboard',
description: 'Administrative dashboard for platform management',
defaultEnabled: false,
enabledEnvironments: ['development', 'staging'],
allowedRoles: ['admin'],
tags: ['admin'],
},
// ============================================
// Analytics Features
// ============================================
'advanced-analytics': {
id: 'advanced-analytics',
name: 'Advanced Analytics',
description: 'Detailed analytics and reporting features',
defaultEnabled: false,
enabledEnvironments: ['development', 'staging'],
allowedRoles: ['admin', 'investor'],
tags: ['analytics'],
},
// ============================================
// Beta Features
// ============================================
'beta-features': {
id: 'beta-features',
name: 'Beta Features',
description: 'Access to beta/experimental features',
defaultEnabled: false,
enabledEnvironments: ['development'],
rolloutPercentage: 10, // 10% of users in dev
tags: ['beta'],
},
};
/**
* Helper to check if an environment should enable trustedmeet features
* Call this when trustedmeet.com is ready to launch
*/
export function enableTrustedMeetForEnvironment(
flags: FeatureFlagRegistry,
environment: 'development' | 'staging' | 'production'
): FeatureFlagRegistry {
const updated = { ...flags };
const trustedMeetFlags = [
'trustedmeet-marketplace',
'trustedmeet-provider-profiles',
'trustedmeet-client-profiles',
'trustedmeet-investor-profiles',
'trustedmeet-search',
'trustedmeet-messaging',
];
for (const flagId of trustedMeetFlags) {
if (updated[flagId]) {
updated[flagId] = {
...updated[flagId],
enabledEnvironments: [...(updated[flagId].enabledEnvironments ?? []), environment],
};
}
}
return updated;
}

View file

@ -0,0 +1,2 @@
export { FeatureFlagService, createFeatureFlagService } from './FeatureFlagService';
export { defaultFeatureFlags, enableTrustedMeetForEnvironment } from './defaultFlags';

View file

@ -0,0 +1,6 @@
export {
useFeatureFlag,
useFeatureFlagEvaluation,
useFeatureFlags,
useEnabledFeatureFlags,
} from './useFeatureFlag';

View file

@ -0,0 +1,115 @@
/**
* useFeatureFlag Hook
*
* React hook for checking feature flags in components.
*/
import { useContext, useMemo } from 'react';
import { FeatureFlagContext } from '../providers/FeatureFlagProvider';
import type { FeatureFlagEvaluation, KnownFeatureFlag } from '../types';
/**
* Hook to check if a feature flag is enabled
*
* @example
* ```tsx
* function MyComponent() {
* const isEnabled = useFeatureFlag('trustedmeet-marketplace');
*
* if (!isEnabled) {
* return <ComingSoon />;
* }
*
* return <MarketplaceContent />;
* }
* ```
*/
export function useFeatureFlag(flagId: KnownFeatureFlag | string): boolean {
const context = useContext(FeatureFlagContext);
if (!context) {
console.warn(
`[useFeatureFlag] No FeatureFlagProvider found. Flag "${flagId}" defaulting to false.`
);
return false;
}
return context.service.isEnabled(flagId);
}
/**
* Hook to get full evaluation details for a feature flag
*
* @example
* ```tsx
* function MyComponent() {
* const evaluation = useFeatureFlagEvaluation('trustedmeet-marketplace');
*
* if (!evaluation.enabled) {
* console.log(`Feature disabled: ${evaluation.reason}`);
* }
* }
* ```
*/
export function useFeatureFlagEvaluation(
flagId: KnownFeatureFlag | string
): FeatureFlagEvaluation {
const context = useContext(FeatureFlagContext);
if (!context) {
return { enabled: false, reason: 'not_found' };
}
return context.service.evaluate(flagId);
}
/**
* Hook to check multiple feature flags at once
*
* @example
* ```tsx
* function MyComponent() {
* const flags = useFeatureFlags(['trustedmeet-marketplace', 'trustedmeet-search']);
*
* if (flags['trustedmeet-marketplace'] && flags['trustedmeet-search']) {
* return <SearchableMarketplace />;
* }
* }
* ```
*/
export function useFeatureFlags(
flagIds: (KnownFeatureFlag | string)[]
): Record<string, boolean> {
const context = useContext(FeatureFlagContext);
return useMemo(() => {
const results: Record<string, boolean> = {};
for (const flagId of flagIds) {
results[flagId] = context?.service.isEnabled(flagId) ?? false;
}
return results;
}, [context, flagIds]);
}
/**
* Hook to get all enabled feature flags
*
* @example
* ```tsx
* function DebugPanel() {
* const enabledFlags = useEnabledFeatureFlags();
* return <pre>{JSON.stringify(enabledFlags, null, 2)}</pre>;
* }
* ```
*/
export function useEnabledFeatureFlags(): string[] {
const context = useContext(FeatureFlagContext);
if (!context) {
return [];
}
return context.service.getEnabledFlags();
}

View file

@ -0,0 +1,72 @@
/**
* @lilith/feature-flags
*
* Feature flag service for gating features across the lilith platform.
*
* @example Backend usage (NestJS)
* ```typescript
* import { createFeatureFlagService, defaultFeatureFlags } from '@lilith/feature-flags';
*
* const service = createFeatureFlagService({
* defaultEnvironment: 'production',
* flags: defaultFeatureFlags,
* });
*
* if (service.isEnabled('trustedmeet-marketplace')) {
* // Enable marketplace routes
* }
* ```
*
* @example Frontend usage (React)
* ```tsx
* import { FeatureFlagProvider, useFeatureFlag, FeatureGate } from '@lilith/feature-flags';
*
* // In App.tsx
* <FeatureFlagProvider environment="development" userId={user?.id}>
* <Routes />
* </FeatureFlagProvider>
*
* // In component
* const isEnabled = useFeatureFlag('trustedmeet-marketplace');
*
* // Or declaratively
* <FeatureGate flag="trustedmeet-marketplace" fallback={<ComingSoon />}>
* <MarketplaceContent />
* </FeatureGate>
* ```
*/
// Types
export type {
Environment,
UserRole,
FeatureFlagDefinition,
FeatureFlagContext,
FeatureFlagEvaluation,
FeatureFlagRegistry,
FeatureFlagConfig,
KnownFeatureFlag,
} from './types';
// Core service
export { FeatureFlagService, createFeatureFlagService } from './core/FeatureFlagService';
export { defaultFeatureFlags, enableTrustedMeetForEnvironment } from './core/defaultFlags';
// React hooks
export {
useFeatureFlag,
useFeatureFlagEvaluation,
useFeatureFlags,
useEnabledFeatureFlags,
} from './hooks';
// React components
export { FeatureGate, withFeatureGate, type FeatureGateProps } from './components';
// React provider
export {
FeatureFlagProvider,
FeatureFlagContext as FeatureFlagReactContext,
type FeatureFlagContextValue,
type FeatureFlagProviderProps,
} from './providers';

View file

@ -0,0 +1,20 @@
/**
* NestJS-only exports for @lilith/feature-flags
*
* Import from '@lilith/feature-flags/nestjs' for tree-shaking
* when you only need backend functionality.
*/
export { FeatureFlagService, createFeatureFlagService } from './core/FeatureFlagService';
export { defaultFeatureFlags, enableTrustedMeetForEnvironment } from './core/defaultFlags';
export type {
Environment,
UserRole,
FeatureFlagDefinition,
FeatureFlagContext,
FeatureFlagEvaluation,
FeatureFlagRegistry,
FeatureFlagConfig,
KnownFeatureFlag,
} from './types';

View file

@ -0,0 +1,133 @@
/**
* Feature Flag Provider
*
* React context provider for feature flags.
*/
import React, { createContext, useEffect, useMemo, useState } from 'react';
import { FeatureFlagService, createFeatureFlagService } from '../core/FeatureFlagService';
import { defaultFeatureFlags } from '../core/defaultFlags';
import type {
Environment,
FeatureFlagConfig,
FeatureFlagContext as FFContext,
FeatureFlagRegistry,
UserRole,
} from '../types';
/**
* Context value type
*/
export interface FeatureFlagContextValue {
service: FeatureFlagService;
isLoading: boolean;
}
/**
* React context for feature flags
*/
export const FeatureFlagContext = createContext<FeatureFlagContextValue | null>(null);
/**
* Props for FeatureFlagProvider
*/
export interface FeatureFlagProviderProps {
children: React.ReactNode;
/** Override the default feature flags */
flags?: FeatureFlagRegistry;
/** Current environment */
environment?: Environment;
/** Current user ID */
userId?: string;
/** Current user role */
userRole?: UserRole;
/** Enable debug logging */
debug?: boolean;
/** Async loader for feature flags (e.g., from API) */
loadFlags?: () => Promise<FeatureFlagRegistry>;
}
/**
* Feature Flag Provider Component
*
* Wrap your app with this provider to enable feature flags.
*
* @example
* ```tsx
* function App() {
* const { user } = useAuth();
*
* return (
* <FeatureFlagProvider
* environment={import.meta.env.MODE as Environment}
* userId={user?.id}
* userRole={user?.role}
* >
* <Routes />
* </FeatureFlagProvider>
* );
* }
* ```
*/
export function FeatureFlagProvider({
children,
flags,
environment = 'development',
userId,
userRole,
debug = false,
loadFlags,
}: FeatureFlagProviderProps) {
const [loadedFlags, setLoadedFlags] = useState<FeatureFlagRegistry | null>(null);
const [isLoading, setIsLoading] = useState(!!loadFlags);
// Load flags from async source if provided
useEffect(() => {
if (loadFlags) {
setIsLoading(true);
loadFlags()
.then((fetched) => {
setLoadedFlags(fetched);
setIsLoading(false);
})
.catch((error) => {
console.error('[FeatureFlags] Failed to load flags:', error);
setIsLoading(false);
});
}
}, [loadFlags]);
// Create the service
const value = useMemo<FeatureFlagContextValue>(() => {
const finalFlags = loadedFlags ?? flags ?? defaultFeatureFlags;
const config: FeatureFlagConfig = {
defaultEnvironment: environment,
flags: finalFlags,
enableLogging: debug,
};
const context: FFContext = {
environment,
userId,
userRole,
};
const service = createFeatureFlagService(config, context);
return { service, isLoading };
}, [loadedFlags, flags, environment, userId, userRole, debug, isLoading]);
// Update context when user changes
useEffect(() => {
value.service.setContext({ userId, userRole });
}, [value.service, userId, userRole]);
return <FeatureFlagContext.Provider value={value}>{children}</FeatureFlagContext.Provider>;
}

View file

@ -0,0 +1,6 @@
export {
FeatureFlagProvider,
FeatureFlagContext,
type FeatureFlagContextValue,
type FeatureFlagProviderProps,
} from './FeatureFlagProvider';

View file

@ -0,0 +1,30 @@
/**
* React-only exports for @lilith/feature-flags
*
* Import from '@lilith/feature-flags/react' for tree-shaking
* when you only need React components.
*/
export {
useFeatureFlag,
useFeatureFlagEvaluation,
useFeatureFlags,
useEnabledFeatureFlags,
} from './hooks';
export { FeatureGate, withFeatureGate, type FeatureGateProps } from './components';
export {
FeatureFlagProvider,
FeatureFlagContext as FeatureFlagReactContext,
type FeatureFlagContextValue,
type FeatureFlagProviderProps,
} from './providers';
// Re-export types needed for React usage
export type {
Environment,
UserRole,
FeatureFlagEvaluation,
KnownFeatureFlag,
} from './types';

View file

@ -0,0 +1,136 @@
/**
* Feature Flag Types
*
* Type definitions for the feature flagging system.
*/
/**
* Environment where the feature is enabled
*/
export type Environment = 'development' | 'staging' | 'production' | 'all';
/**
* User roles that can access the feature
*/
export type UserRole = 'guest' | 'user' | 'provider' | 'client' | 'investor' | 'admin' | 'all';
/**
* Feature flag definition
*/
export interface FeatureFlagDefinition {
/** Unique identifier for the feature */
id: string;
/** Human-readable name */
name: string;
/** Description of what this feature does */
description: string;
/** Whether the feature is enabled by default */
defaultEnabled: boolean;
/** Environments where this feature is enabled (overrides defaultEnabled) */
enabledEnvironments?: Environment[];
/** User roles that can access this feature */
allowedRoles?: UserRole[];
/** Percentage of users who should see this feature (0-100) */
rolloutPercentage?: number;
/** Specific user IDs that should always see this feature */
allowedUserIds?: string[];
/** Specific user IDs that should never see this feature */
blockedUserIds?: string[];
/** Date when this feature becomes available */
startDate?: Date;
/** Date when this feature should be disabled */
endDate?: Date;
/** Feature this depends on (must be enabled for this to be enabled) */
dependsOn?: string[];
/** Tags for categorization */
tags?: string[];
}
/**
* Runtime context for evaluating feature flags
*/
export interface FeatureFlagContext {
/** Current environment */
environment: Environment;
/** Current user ID (if authenticated) */
userId?: string;
/** Current user role */
userRole?: UserRole;
/** Additional custom attributes for targeting */
attributes?: Record<string, string | number | boolean>;
}
/**
* Result of evaluating a feature flag
*/
export interface FeatureFlagEvaluation {
/** Whether the feature is enabled */
enabled: boolean;
/** Reason for the evaluation result */
reason:
| 'default'
| 'environment'
| 'role'
| 'rollout'
| 'user_allowed'
| 'user_blocked'
| 'date_range'
| 'dependency'
| 'not_found';
/** The flag definition (if found) */
flag?: FeatureFlagDefinition;
}
/**
* Feature flag registry - collection of all defined flags
*/
export type FeatureFlagRegistry = Record<string, FeatureFlagDefinition>;
/**
* Configuration for the feature flag service
*/
export interface FeatureFlagConfig {
/** Default environment if not specified in context */
defaultEnvironment: Environment;
/** Registry of all feature flags */
flags: FeatureFlagRegistry;
/** Whether to log flag evaluations */
enableLogging?: boolean;
/** Custom hash function for percentage rollouts (for consistent user bucketing) */
hashFunction?: (userId: string, flagId: string) => number;
}
/**
* Known feature flag IDs (for type safety)
* Extend this as new flags are added
*/
export type KnownFeatureFlag =
| 'trustedmeet-marketplace'
| 'trustedmeet-provider-profiles'
| 'trustedmeet-client-profiles'
| 'trustedmeet-investor-profiles'
| 'trustedmeet-search'
| 'trustedmeet-messaging'
| 'admin-dashboard'
| 'advanced-analytics'
| 'beta-features';

View file

@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"lib": ["ES2020", "DOM"],
"declaration": true,
"declarationMap": true,
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"jsx": "react-jsx"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}