UNPKG

better-auth-feature-flags

Version:

Ship features safely with feature flags, A/B testing, and progressive rollouts - Better Auth plugin for modern release management

354 lines (345 loc) 11.1 kB
import { BetterAuthPlugin } from 'better-auth'; import * as z from 'zod'; /** * Module augmentation for Better Auth types * This extends Better Auth's built-in types with feature flags support */ declare module "better-auth" { } /** * Runtime validation schemas. Rejects invalid input before DB write. * * @invariant Flag keys: /^[a-z0-9-_]+$/i (URL-safe) * @invariant Percentages: 0 ≤ n ≤ 100 * @invariant Variant weights: Σ(weights) = 100 ± 0.01 * @invariant Priority: integer, -1000 ≤ n ≤ 1000 * * @decision 0.01 tolerance for variant weights handles IEEE 754 precision * @decision Empty variants array allowed (feature without A/B test) */ declare const flagTypeSchema: z.ZodEnum<{ string: "string"; number: "number"; boolean: "boolean"; json: "json"; }>; declare const evaluationReasonSchema: z.ZodEnum<{ default: "default"; rule_match: "rule_match"; override: "override"; percentage_rollout: "percentage_rollout"; disabled: "disabled"; not_found: "not_found"; }>; declare const auditActionSchema: z.ZodEnum<{ enabled: "enabled"; disabled: "disabled"; created: "created"; updated: "updated"; deleted: "deleted"; rule_added: "rule_added"; rule_updated: "rule_updated"; rule_deleted: "rule_deleted"; override_added: "override_added"; override_removed: "override_removed"; }>; declare const conditionOperatorSchema: z.ZodEnum<{ equals: "equals"; not_equals: "not_equals"; contains: "contains"; not_contains: "not_contains"; starts_with: "starts_with"; ends_with: "ends_with"; greater_than: "greater_than"; less_than: "less_than"; greater_than_or_equal: "greater_than_or_equal"; less_than_or_equal: "less_than_or_equal"; in: "in"; not_in: "not_in"; regex: "regex"; }>; declare const conditionSchema: z.ZodObject<{ attribute: z.ZodString; operator: z.ZodEnum<{ equals: "equals"; not_equals: "not_equals"; contains: "contains"; not_contains: "not_contains"; starts_with: "starts_with"; ends_with: "ends_with"; greater_than: "greater_than"; less_than: "less_than"; greater_than_or_equal: "greater_than_or_equal"; less_than_or_equal: "less_than_or_equal"; in: "in"; not_in: "not_in"; regex: "regex"; }>; value: z.ZodAny; }, z.core.$strip>; /** * Recursive targeting conditions. * @example { all: [{ attribute: "role", operator: "equals", value: "admin" }] } * @invariant Evaluation order: NOT → ALL → ANY * @invariant {} matches all users */ declare const ruleConditionsSchema: z.ZodType<{ all?: z.infer<typeof conditionSchema>[]; any?: z.infer<typeof conditionSchema>[]; not?: any; }>; declare const flagRuleInputSchema: z.ZodObject<{ name: z.ZodOptional<z.ZodString>; priority: z.ZodDefault<z.ZodNumber>; conditions: z.ZodType<{ all?: z.infer<typeof conditionSchema>[]; any?: z.infer<typeof conditionSchema>[]; not?: any; }, unknown, z.core.$ZodTypeInternals<{ all?: z.infer<typeof conditionSchema>[]; any?: z.infer<typeof conditionSchema>[]; not?: any; }, unknown>>; value: z.ZodAny; percentage: z.ZodOptional<z.ZodNumber>; enabled: z.ZodDefault<z.ZodBoolean>; }, z.core.$strip>; declare const featureFlagInputSchema: z.ZodObject<{ key: z.ZodString; name: z.ZodString; description: z.ZodOptional<z.ZodString>; type: z.ZodDefault<z.ZodEnum<{ string: "string"; number: "number"; boolean: "boolean"; json: "json"; }>>; enabled: z.ZodDefault<z.ZodBoolean>; defaultValue: z.ZodOptional<z.ZodAny>; rolloutPercentage: z.ZodDefault<z.ZodNumber>; variants: z.ZodOptional<z.ZodArray<z.ZodObject<{ key: z.ZodString; value: z.ZodAny; weight: z.ZodNumber; metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodAny>>; }, z.core.$strip>>>; metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodAny>>; }, z.core.$strip>; declare const evaluationContextSchema: z.ZodObject<{ userId: z.ZodOptional<z.ZodString>; email: z.ZodOptional<z.ZodString>; role: z.ZodOptional<z.ZodString>; organizationId: z.ZodOptional<z.ZodString>; attributes: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodAny>>; }, z.core.$strip>; declare const flagOverrideInputSchema: z.ZodObject<{ flagId: z.ZodString; userId: z.ZodString; value: z.ZodAny; reason: z.ZodOptional<z.ZodString>; expiresAt: z.ZodOptional<z.ZodDate>; }, z.core.$strip>; declare const flagEvaluationInputSchema: z.ZodObject<{ flagId: z.ZodString; userId: z.ZodOptional<z.ZodString>; sessionId: z.ZodOptional<z.ZodString>; value: z.ZodAny; variant: z.ZodOptional<z.ZodString>; reason: z.ZodOptional<z.ZodEnum<{ default: "default"; rule_match: "rule_match"; override: "override"; percentage_rollout: "percentage_rollout"; disabled: "disabled"; not_found: "not_found"; }>>; context: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodAny>>; }, z.core.$strip>; declare const flagAuditInputSchema: z.ZodObject<{ flagId: z.ZodString; userId: z.ZodOptional<z.ZodString>; action: z.ZodEnum<{ enabled: "enabled"; disabled: "disabled"; created: "created"; updated: "updated"; deleted: "deleted"; rule_added: "rule_added"; rule_updated: "rule_updated"; rule_deleted: "rule_deleted"; override_added: "override_added"; override_removed: "override_removed"; }>; previousValue: z.ZodOptional<z.ZodAny>; newValue: z.ZodOptional<z.ZodAny>; metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodAny>>; }, z.core.$strip>; /** * TypeScript type definitions for compile-time safety. * * @decision Entity types = Zod infer + DB fields (id, timestamps) * @decision Dates as Date objects, not strings, for type safety * @performance CacheConfig.ttl: 60-300s (balance freshness vs load) * @performance AnalyticsConfig.sampleRate: 0.01-0.1 (high traffic) */ type FlagType = z.infer<typeof flagTypeSchema>; type EvaluationReason = z.infer<typeof evaluationReasonSchema>; type AuditAction = z.infer<typeof auditActionSchema>; type ConditionOperator = z.infer<typeof conditionOperatorSchema>; type RuleConditions = z.infer<typeof ruleConditionsSchema>; type EvaluationContext = z.infer<typeof evaluationContextSchema>; type FeatureFlag = z.infer<typeof featureFlagInputSchema> & { id: string; organizationId?: string; createdAt: Date; updatedAt: Date; }; type FlagRule = z.infer<typeof flagRuleInputSchema> & { id: string; flagId: string; createdAt: Date; }; type FlagOverride = z.infer<typeof flagOverrideInputSchema> & { id: string; createdAt: Date; }; type FlagEvaluation = z.infer<typeof flagEvaluationInputSchema> & { id: string; evaluatedAt: Date; }; type FlagAudit = z.infer<typeof flagAuditInputSchema> & { id: string; createdAt: Date; }; /** * Security validation utilities for feature flags context data */ /** * Configuration for header validation */ interface HeaderConfig { name: string; type: "string" | "number" | "boolean" | "json" | "enum"; maxLength?: number; enumValues?: string[]; pattern?: RegExp; required?: boolean; sanitize?: (value: string) => string; } /** * Configuration for context data validation */ interface ValidationConfig { maxStringLength?: number; maxObjectDepth?: number; maxArrayLength?: number; maxTotalSize?: number; allowedKeyPattern?: RegExp; } /** * Default header configuration with secure validation rules */ declare const DEFAULT_HEADER_CONFIG: HeaderConfig[]; /** * Options for controlling what context data to collect */ interface ContextCollectionOptions { /** Collect device type, browser, OS from user agent */ collectDevice?: boolean; /** Collect geographic data from CDN headers */ collectGeo?: boolean; /** Collect custom x-feature-flag-* and x-targeting-* headers */ collectCustomHeaders?: boolean; /** Collect client info like IP, referrer */ collectClientInfo?: boolean; /** Specific attributes to collect (whitelist) */ allowedAttributes?: string[]; } interface FeatureFlagsOptions { flags?: { [key: string]: { enabled?: boolean; default?: boolean; rollout?: number; targeting?: { roles?: string[]; userIds?: string[]; attributes?: Record<string, any>; }; variants?: { [key: string]: any; }; }; }; storage?: "memory" | "database" | "redis"; analytics?: { trackUsage?: boolean; trackPerformance?: boolean; }; adminAccess?: { enabled?: boolean; roles?: string[]; }; multiTenant?: { enabled?: boolean; useOrganizations?: boolean; }; caching?: { enabled?: boolean; ttl?: number; }; audit?: { enabled?: boolean; retentionDays?: number; }; /** * Configure what context data to collect for flag evaluation. * By default, only basic session data is collected for privacy. */ contextCollection?: ContextCollectionOptions; /** * Configure custom header processing for feature flag evaluation. * Provides a secure whitelist-based approach for header extraction. */ customHeaders?: { enabled?: boolean; whitelist?: HeaderConfig[]; strict?: boolean; logInvalid?: boolean; }; /** * Configure validation rules for context data. * Helps prevent memory exhaustion and security issues. */ contextValidation?: ValidationConfig; } /** * Better Auth Feature Flags Plugin * * Provides comprehensive feature flag management with: * - Multiple storage backends (memory, database, redis) * - Advanced targeting and segmentation * - Percentage-based rollouts * - A/B testing with variants * - Real-time evaluation and caching * - Audit logging and analytics * - Multi-tenancy support * * @example * ```typescript * import { betterAuth } from "better-auth"; * import { featureFlags } from "better-auth-feature-flags"; * * export const auth = betterAuth({ * plugins: [ * featureFlags({ * storage: "database", * caching: { enabled: true, ttl: 60 }, * analytics: { trackUsage: true }, * }) * ] * }); * ``` */ declare function featureFlags(options?: FeatureFlagsOptions): BetterAuthPlugin; export { type AuditAction, type ConditionOperator, type ContextCollectionOptions, DEFAULT_HEADER_CONFIG, type EvaluationContext, type EvaluationReason, type FeatureFlag, type FeatureFlagsOptions, type FlagAudit, type FlagEvaluation, type FlagOverride, type FlagRule, type FlagType, type HeaderConfig, type RuleConditions, type ValidationConfig, featureFlags as default, featureFlags };