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
TypeScript
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 };