better-auth-feature-flags
Version:
Ship features safely with feature flags, A/B testing, and progressive rollouts - Better Auth plugin for modern release management
191 lines (186 loc) • 6.49 kB
TypeScript
import { BetterAuthClientPlugin } from 'better-auth/client';
/**
* Type utilities for flag schema validation and inference
*/
/**
* Extracts boolean-only flag keys from a schema.
* Used to constrain isEnabled() to only accept boolean flags.
*/
type BooleanFlags<Schema extends Record<string, any>> = {
[K in keyof Schema]: Schema[K] extends boolean ? K : never;
}[keyof Schema];
/**
* Validates that a flag schema only contains valid flag value types.
* Prevents using complex objects that can't be properly serialized.
*/
type ValidateFlagSchema<T> = T extends Record<string, any> ? {
[K in keyof T]: T[K] extends boolean | string | number | null | undefined ? T[K] : T[K] extends Array<infer U> ? U extends boolean | string | number | null | undefined ? T[K] : never : never;
} : never;
/**
* Helper type to infer the value type of a specific flag
*/
type InferFlagValue<Schema extends Record<string, any>, K extends keyof Schema> = Schema[K];
/**
* Secure override management for feature flags.
*
* @security Critical: Prevents debug features from leaking to production.
* - Disabled in production environments
* - Automatic expiration to prevent persistent overrides
* - Optional localStorage with encryption consideration
*/
interface OverrideConfig {
/** Allow overrides in production (dangerous!) */
allowInProduction?: boolean;
/** Override expiration time in ms (default: 1 hour) */
ttl?: number;
/** Persist overrides to localStorage */
persist?: boolean;
/** Storage key prefix */
keyPrefix?: string;
/** Environment detection override for testing */
environment?: "development" | "production";
}
interface FeatureFlagVariant {
key: string;
value: any;
percentage?: number;
}
interface FeatureFlagResult {
value: any;
variant?: FeatureFlagVariant;
reason: "default" | "rule_match" | "override" | "percentage" | "not_found" | "disabled";
}
/**
* Feature flags client configuration options.
*
* @template Schema - Optional flag schema for type safety
*/
interface FeatureFlagsClientOptions<Schema extends Record<string, any> = Record<string, any>> {
/**
* Cache configuration.
* Controls client-side flag caching for performance and offline support.
*/
cache?: {
enabled?: boolean;
ttl?: number;
storage?: "memory" | "localStorage" | "sessionStorage";
keyPrefix?: string;
version?: string;
include?: string[];
exclude?: string[];
};
/**
* Smart polling with exponential backoff and jitter.
* Prevents thundering herd and gracefully handles server issues.
*/
polling?: {
enabled?: boolean;
interval?: number;
};
/**
* Default flag values
*/
defaults?: Partial<Schema>;
/**
* Debug mode
*/
debug?: boolean;
/**
* Error handler
*/
onError?: (error: Error) => void;
/**
* Evaluation callback
*/
onEvaluation?: (flag: string, result: any) => void;
/**
* Context sanitization settings.
* Prevents PII leakage and enforces size limits.
*/
contextSanitization?: {
enabled?: boolean;
strict?: boolean;
allowedFields?: string[];
maxUrlSize?: number;
maxBodySize?: number;
warnOnDrop?: boolean;
};
/**
* Override configuration for local testing.
* @security Automatically disabled in production unless explicitly allowed.
*/
overrides?: OverrideConfig;
}
interface EvaluationContext {
attributes?: Record<string, any>;
device?: string;
browser?: string;
version?: string;
[key: string]: any;
}
/**
* Type-safe feature flags client interface.
*
* @template Schema - Optional flag schema for type safety.
* Defaults to Record<string, any> for backward compatibility.
*
* @example
* ```typescript
* // Define your flag schema
* interface MyFlags {
* "feature.darkMode": boolean;
* "experiment.algorithm": "A" | "B" | "C";
* "config.maxItems": number;
* }
*
* // Use with type safety
* const client: FeatureFlagsClient<MyFlags> = createAuthClient();
* ```
*/
interface FeatureFlagsClient<Schema extends Record<string, any> = Record<string, any>> {
featureFlags: {
isEnabled: <K extends keyof Schema>(flag: K & {
[P in K]: Schema[P] extends boolean ? K : never;
}[K], defaultValue?: boolean) => Promise<boolean>;
getValue: <K extends keyof Schema>(flag: K, defaultValue?: Schema[K]) => Promise<Schema[K]>;
getVariant: <K extends keyof Schema>(flag: K) => Promise<FeatureFlagVariant | null>;
getAllFlags: () => Promise<Partial<Schema>>;
evaluateBatch: <K extends keyof Schema>(flags: K[]) => Promise<Record<K, FeatureFlagResult>>;
track: <K extends keyof Schema>(flag: K, event: string, value?: number | Record<string, any>) => Promise<void>;
setContext: (context: Partial<EvaluationContext>) => void;
getContext: () => EvaluationContext;
prefetch: <K extends keyof Schema>(flags: K[]) => Promise<void>;
clearCache: () => void;
setOverride: <K extends keyof Schema>(flag: K, value: Schema[K]) => void;
clearOverrides: () => void;
refresh: () => Promise<void>;
subscribe: (callback: (flags: Partial<Schema>) => void) => () => void;
dispose?: () => void;
};
}
/**
* Creates a type-safe feature flags client plugin for Better Auth.
*
* @template Schema - Optional flag schema for full type safety.
* When provided, all flag operations will be type-checked.
*
* @example
* ```typescript
* // Without schema (backward compatible)
* const client = createAuthClient({
* plugins: [featureFlagsClient()]
* });
*
* // With type-safe schema
* interface MyFlags {
* "feature.darkMode": boolean;
* "experiment.variant": "A" | "B" | "C";
* }
*
* const client = createAuthClient({
* plugins: [featureFlagsClient<MyFlags>()]
* });
* ```
*/
declare function featureFlagsClient<Schema extends Record<string, any> = Record<string, any>>(options?: FeatureFlagsClientOptions<Schema>): BetterAuthClientPlugin;
export { type BooleanFlags, type EvaluationContext, type FeatureFlagResult, type FeatureFlagVariant, type FeatureFlagsClient, type FeatureFlagsClientOptions, type InferFlagValue, type ValidateFlagSchema, featureFlagsClient as default, featureFlagsClient };