flagmint-js-sdk 1.2.26 → 2.2.0

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.
package/dist/index.d.ts CHANGED
@@ -5,7 +5,7 @@ export * as syncCache from './sdk/core/helpers/cacheHelper';
5
5
  export * as asyncCache from './sdk/core/helpers/cacheHelper.async';
6
6
  export type { Transport } from './sdk/core/transports/Transport';
7
7
  export { LongPollingTransport } from './sdk/core/transports/LongPollingTransport';
8
- export { WebSocketTransport } from './sdk/core/transports/WebsocketTransport';
8
+ export { SseTransport } from './sdk/core/transports/SSETransport';
9
9
  export type { FlagValue, EvaluationContext, Segment, Rollout, VariantRollout, VariantOption } from './sdk/core/evaluation/types';
10
10
  export { evaluateFlagValue } from './sdk/core/evaluation/evaluateFlagValue';
11
11
  export { evaluateRollout } from './sdk/core/evaluation/evaluateRollout';
@@ -1,7 +1,7 @@
1
1
  import { CacheAdapter, FeatureFlags } from '../core/helpers/types';
2
2
  import type { Transport } from '../core/transports/Transport';
3
3
  import { FlagValue } from '../core/evaluation/types';
4
- type TransportMode = 'auto' | 'websocket' | 'long-polling';
4
+ type TransportMode = 'auto' | 'long-polling' | 'sse';
5
5
  export interface FlagClientOptions<C extends Record<string, any> = Record<string, any>> {
6
6
  apiKey: string;
7
7
  context?: C;
@@ -28,10 +28,15 @@ export interface FlagClientOptions<C extends Record<string, any> = Record<string
28
28
  deferInitialization?: boolean;
29
29
  cacheAdapter?: CacheAdapter<C>;
30
30
  restEndpoint?: string;
31
- wsEndpoint?: string;
31
+ sseEndpoint?: string;
32
32
  debugLog?: boolean;
33
33
  env?: string;
34
34
  enableFlagmint: boolean;
35
+ wrapperInfo?: {
36
+ name: string;
37
+ version: string;
38
+ };
39
+ EventSourceImpl?: any;
35
40
  }
36
41
  type FlagUpdateCallback<T> = (flags: FeatureFlags<T>) => void;
37
42
  export declare class FlagClient<T = unknown, C extends Record<string, any> = Record<string, any>> {
@@ -44,7 +49,8 @@ export declare class FlagClient<T = unknown, C extends Record<string, any> = Rec
44
49
  private cacheTTL;
45
50
  private transport;
46
51
  private restEndpoint;
47
- private wsEndpoint;
52
+ private aslHandshakeUrl;
53
+ private sseEndpoint;
48
54
  private readyPromise;
49
55
  private resolveReady;
50
56
  private rejectReady;
@@ -68,8 +74,33 @@ export declare class FlagClient<T = unknown, C extends Record<string, any> = Rec
68
74
  */
69
75
  private initialize;
70
76
  /**
71
- * Sets up the transport layer for the client.
72
- */
77
+ * Creates and initializes the transport layer used to receive feature flag
78
+ * updates from the Flagmint platform.
79
+ *
80
+ * This method performs the authentication handshake to obtain a fresh session
81
+ * identifier, selects the appropriate transport based on the configured
82
+ * transport mode, and initializes it before attaching the internal flag update
83
+ * listener.
84
+ *
85
+ * When operating in `auto` mode, the client attempts to establish a
86
+ * Server-Sent Events (SSE) connection first. If SSE initialization fails,
87
+ * it automatically falls back to the long-polling transport to maintain
88
+ * feature flag delivery.
89
+ *
90
+ * Once the transport is ready, any available initial flags are immediately
91
+ * synchronized into the client state before future updates are received
92
+ * through the transport's notification callbacks.
93
+ *
94
+ * @param {FlagClientOptions<C>} options - The client configuration used to
95
+ * authenticate the handshake request and determine which transport strategy
96
+ * to initialize.
97
+ *
98
+ * @returns {Promise<void>} A promise that resolves once the selected transport
99
+ * has been initialized and wired into the FlagClient.
100
+ *
101
+ * @throws {Error} If the authentication handshake fails, the session identifier
102
+ * cannot be obtained, or the configured transport fails to initialize.
103
+ */
73
104
  private setupTransport;
74
105
  /**
75
106
  * Updates flags and notifies all subscribers.
@@ -103,15 +134,72 @@ export declare class FlagClient<T = unknown, C extends Record<string, any> = Rec
103
134
  */
104
135
  destroy(): void;
105
136
  /**
106
- * Wait for the client to be ready.
107
- * Resolves when: flags are available AND initialization succeeded.
108
- * Rejects if: initialization/connection fails (even if cached flags exist).
109
- */
137
+ * Waits until the FlagClient is fully initialized and ready for feature flag
138
+ * evaluation.
139
+ *
140
+ * If deferred initialization is enabled, this method will first perform the
141
+ * initial client setup. It then waits for the first feature flag payload to
142
+ * become available (or until the specified timeout expires) before awaiting
143
+ * completion of the underlying transport initialization.
144
+ *
145
+ * This method guarantees that transport or connection failures are surfaced,
146
+ * even when feature flags have already been restored from cache.
147
+ *
148
+ * @param {number} [timeoutMs=3000] - The maximum amount of time, in
149
+ * milliseconds, to wait for the initial feature flag payload before
150
+ * continuing initialization.
151
+ *
152
+ * @returns {Promise<void>} A promise that resolves when the client is fully
153
+ * initialized and ready to evaluate feature flags.
154
+ *
155
+ * @throws {Error} If client initialization or the underlying transport fails,
156
+ * such as authentication, rate limiting, or connection errors.
157
+ */
110
158
  ready(timeoutMs?: number): Promise<void>;
111
159
  /**
112
160
  * Evaluate flags locally (for preview mode).
113
161
  */
114
162
  private evaluateLocally;
163
+ /**
164
+ * Waits until the client receives its first non-empty feature flag payload,
165
+ * or until the specified timeout elapses.
166
+ *
167
+ * This method subscribes to flag updates and resolves as soon as at least one
168
+ * feature flag is available. If no flags are received within the allotted
169
+ * timeout, the promise still resolves, allowing the SDK to continue
170
+ * initialization using default or cached values instead of blocking
171
+ * indefinitely.
172
+ *
173
+ * The internal subscription is automatically removed when either condition
174
+ * is met to prevent memory leaks.
175
+ *
176
+ * @param {number} timeoutMs - The maximum amount of time, in milliseconds,
177
+ * to wait for the initial feature flag payload before resolving.
178
+ *
179
+ * @returns {Promise<void>} A promise that resolves when the first non-empty
180
+ * flag payload is received or when the timeout expires, whichever occurs first.
181
+ */
115
182
  private waitForFlags;
183
+ /**
184
+ * Performs the initial authentication handshake with the Flagmint server
185
+ * to obtain a fresh, single-use session identifier.
186
+ *
187
+ * The returned session ID is used by streaming transports (SSE/WebSocket)
188
+ * to establish an authenticated real-time connection. Session identifiers
189
+ * are intentionally short-lived and should not be reused across new
190
+ * streaming connections.
191
+ *
192
+ * @param {string} apiKey - The Flagmint API key used to authenticate the
193
+ * handshake request.
194
+ *
195
+ * @returns {Promise<string>} A promise that resolves with a newly issued
196
+ * session identifier.
197
+ *
198
+ * @throws {Error} If the API credentials are invalid (`ERR_AUTH`).
199
+ * @throws {Error} If the client has exceeded its rate limit (`ERR_RATE_LIMITED`).
200
+ * @throws {Error} If the handshake endpoint returns an unexpected HTTP error.
201
+ * @throws {Error} If the handshake response does not contain a valid session ID.
202
+ */
203
+ private fetchFreshSessionId;
116
204
  }
117
205
  export {};
@@ -0,0 +1,32 @@
1
+ /** Must match FF-EU `utils/config-sync/asl-ecdh.ts`. */
2
+ export declare const ASL_CLIENT_PUBLIC_KEY_HEX_LENGTH = 64;
3
+ export declare const ASL_HKDF_INFO = "flagmint-asl-config-sync-mac-v1";
4
+ export declare const ASL_KEY_AGREEMENT: "x25519-hkdf-sha256";
5
+ export type AslClientKeyPair = {
6
+ /** Raw X25519 public key — 64 lowercase hex chars. */
7
+ publicKeyHex: string;
8
+ /** Keep private; never send on the wire. */
9
+ privateKey: Uint8Array;
10
+ };
11
+ export type AslDerivedMac = {
12
+ /** 32-byte session MAC key — never sent on the wire. */
13
+ configMacKey: Uint8Array;
14
+ };
15
+ /**
16
+ * Generate an ephemeral X25519 keypair for the ASL handshake ECDH.
17
+ */
18
+ export declare function generateAslClientKeyPair(): AslClientKeyPair;
19
+ export declare function parsePeerPublicKeyHex(hex: string): Uint8Array;
20
+ /** HKDF salt from the handshake — public; must be even-length hex (FF-EU uses 16 bytes). */
21
+ export declare function parseSaltHex(hex: string): Uint8Array;
22
+ /**
23
+ * Client half of X25519 ECDH + HKDF.
24
+ * Derives the same MAC key as the server without transmitting the secret.
25
+ */
26
+ export declare function deriveAslMacKey(input: {
27
+ privateKey: Uint8Array;
28
+ peerPublicKeyHex: string;
29
+ saltHex: string;
30
+ }): AslDerivedMac;
31
+ /** Zeroize a key buffer when the session ends (best-effort). */
32
+ export declare function wipeKeyMaterial(key: Uint8Array | null | undefined): void;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Top-level SdkFlagConfig evaluation — mirrors FF-EU RuleEngine.compileFlagToPredicate
3
+ * using the ported `flagEvaluator` (spec = utils/flag-evaluator.ts).
4
+ */
5
+ import { type FlagEvaluationResult } from './flagEvaluator';
6
+ import type { SdkFlagConfig, SdkSegment } from './types';
7
+ /** ES2018-safe Map → record (avoids Object.fromEntries / ES2019 lib). */
8
+ export declare function mapToRecord<V>(map: Map<string, V>): Record<string, V>;
9
+ /**
10
+ * Prepare context for flag-evaluator.
11
+ *
12
+ * - Structured `kind` contexts pass through (evaluator flattens like the server).
13
+ * - Already-flat SSE maps pass through.
14
+ * - Nested SDK `{ user, organization, custom }` without `kind` →
15
+ * `flattenEvaluationContext` (kind-prefixed) so multi-kind attrs do not collide
16
+ * and organization is not dropped.
17
+ */
18
+ export declare function prepareContextForEvaluator(context: Record<string, unknown> | null | undefined): Record<string, unknown>;
19
+ /**
20
+ * Evaluate one SdkFlagConfig against context (server RuleEngine order).
21
+ */
22
+ export declare function evaluateSdkFlag(flag: SdkFlagConfig, context: Record<string, unknown>, segments: Map<string, SdkSegment> | Record<string, SdkSegment>): FlagEvaluationResult;
23
+ export declare function evaluateAllSdkFlags<T = unknown>(flags: Map<string, SdkFlagConfig> | SdkFlagConfig[], context: Record<string, unknown>, segments: Map<string, SdkSegment> | Record<string, SdkSegment>, options?: {
24
+ failClosedDefaultsOnly?: boolean;
25
+ }): Record<string, T>;
26
+ export { coerceType } from './flagEvaluator';
@@ -0,0 +1,373 @@
1
+ export type ExpectedFlagType = 'boolean' | 'string' | 'number' | 'json';
2
+ export type FlagEvaluationResult = boolean | string | number | Record<string, any>;
3
+ export type FlatContext = Record<string, any>;
4
+ export type Condition = {
5
+ attribute: string;
6
+ operator: string;
7
+ value?: unknown;
8
+ };
9
+ export type TargetingRule = {
10
+ id?: string;
11
+ kind: 'segment' | 'custom' | string;
12
+ order_index: number;
13
+ segment_id?: string | null;
14
+ conditions?: Condition[];
15
+ logical_op?: string;
16
+ variation_id?: string | null;
17
+ rollout_id?: string | null;
18
+ };
19
+ export type Variation = {
20
+ id: string;
21
+ value: unknown;
22
+ key?: string;
23
+ name?: string;
24
+ type?: string;
25
+ description?: string;
26
+ [key: string]: unknown;
27
+ };
28
+ export type Rollout = {
29
+ strategy: 'off';
30
+ } | {
31
+ strategy: 'percentage';
32
+ percentage: number;
33
+ salt: string;
34
+ } | {
35
+ strategy: 'variant';
36
+ salt: string;
37
+ variants: {
38
+ variation_id: string;
39
+ weight: number;
40
+ }[];
41
+ } | {
42
+ strategy: 'gradual';
43
+ salt: string;
44
+ target_percentage: number;
45
+ increment: number;
46
+ interval_hours: number;
47
+ start_at?: string;
48
+ } | {
49
+ strategy?: string;
50
+ [key: string]: any;
51
+ };
52
+ export type Segment = {
53
+ id?: string;
54
+ rules?: Condition[];
55
+ logical_op?: string;
56
+ force?: boolean;
57
+ };
58
+ export type EvaluationDeps = {
59
+ segmentsById?: Record<string, Segment>;
60
+ variationsById: Record<string, Variation>;
61
+ rolloutsById: Record<string, Rollout>;
62
+ getHashPercent?: (input: string) => number;
63
+ };
64
+ type LogicalOperatorType = 'AND' | 'OR' | 'NOT';
65
+ type EvaluationContextT = Record<string, any>;
66
+ type TVariation = Variation;
67
+ /**
68
+ * Evaluate kill switch state.
69
+ *
70
+ * DESIGN PURPOSE: Immediate flag override without evaluating targeting/rollouts.
71
+ *
72
+ * Use cases:
73
+ * - Emergency shutoff: Instantly disable a feature causing issues
74
+ * - Override complex targeting: Kill switch takes precedence over all rules
75
+ * - Safe defaults: Return configured "off" variation instead of arbitrary fallback
76
+ *
77
+ * HOW IT WORKS:
78
+ * 1. If flag is inactive (`is_active=false`) AND has `off_variation_id` configured:
79
+ * - Return the off variation value (kill switch activated)
80
+ * 2. Otherwise:
81
+ * - Return null (kill switch not active, proceed with normal evaluation)
82
+ *
83
+ * EVALUATION PRIORITY:
84
+ * Kill switch is checked FIRST, before:
85
+ * - Targeting rules
86
+ * - Segments
87
+ * - Rollouts
88
+ * - Default values
89
+ *
90
+ * NOTE:
91
+ * - If flag is inactive but no off_variation_id is configured, returns null
92
+ * (caller should fall back to default value)
93
+ * - Variations should be pre-validated; missing variation logs warning
94
+ *
95
+ * @param isActive - Flag's active state
96
+ * @param offVariationId - ID of the variation to serve when flag is off
97
+ * @param variationsById - Map of variation IDs to variations
98
+ * @param expectedType - Expected flag type for coercion
99
+ * @returns Off variation value if kill switch active, null otherwise
100
+ */
101
+ export declare function evaluateKillSwitch(isActive: boolean, offVariationId: string | null | undefined, variationsById: Record<string, TVariation> | undefined, expectedType: ExpectedFlagType): FlagEvaluationResult | null;
102
+ /**
103
+ * Evaluate "on" variation for boolean kill switches.
104
+ *
105
+ * DESIGN PURPOSE: Simple boolean kill switch "ON" state using variations.
106
+ *
107
+ * DESIGN DECISION: Boolean-only kill switches.
108
+ * Flagmint uses kill switches exclusively for boolean flags to maintain simplicity
109
+ * and semantic clarity. For multi-value feature management, use targeting rules
110
+ * or rollout strategies instead.
111
+ *
112
+ * Use cases:
113
+ * - Boolean kill switch: Two variations (true/false), serve true when active
114
+ * - Simple feature flags: No targeting complexity, just on/off states
115
+ * - Emergency controls: Instant enable/disable without complex logic
116
+ *
117
+ * HOW IT WORKS:
118
+ * 1. For boolean flags: Find the variation with value=true
119
+ * 2. If found, return that variation's value
120
+ * 3. Otherwise, return null (caller should fall back to default value)
121
+ *
122
+ * WHEN TO USE:
123
+ * - Flag is active (`is_active=true`)
124
+ * - Flag type is boolean
125
+ * - No targeting rules
126
+ * - No rollouts
127
+ * - Variations are defined
128
+ *
129
+ * NOTE:
130
+ * - This is the complement to evaluateKillSwitch (OFF state)
131
+ * - Only works for boolean flags (kill switches are boolean-only in Flagmint)
132
+ * - For flags with targeting/rollouts, use evaluateFlagWithTargetingRules instead
133
+ *
134
+ * @param variationsById - Map of variation IDs to variations
135
+ * @param expectedType - Expected flag type (must be 'boolean' for kill switches)
136
+ * @returns Variation with value=true if found, null otherwise
137
+ */
138
+ export declare function evaluateOnVariation(variationsById: Record<string, TVariation> | undefined, expectedType: ExpectedFlagType): FlagEvaluationResult | null;
139
+ /**
140
+ * Resolve a targeting attribute against a flat context.
141
+ *
142
+ * SSE flattens with kind prefixes (`user.siteIds`); HTTP flatten leaves
143
+ * user fields bare (`siteIds`) and custom fields as `custom.siteids`.
144
+ * Dashboard custom attributes are stored as `custom.siteids`.
145
+ */
146
+ export declare function getContextAttribute(context: FlatContext, attribute: string): unknown;
147
+ export declare function evaluateRule(condition: Condition, context: FlatContext): boolean;
148
+ /**
149
+ * Coerces the evaluated flag result into its expected type.
150
+ *
151
+ * This helps guarantee SDK and app stability by ensuring that even if the value
152
+ * is malformed or unexpected, the returned type will always match what's expected.
153
+ * For JSON types, validates that the value is actually an object to prevent data corruption.
154
+ *
155
+ * NOTE: We DO NOT throw here to avoid crashing consumers; instead we log + return safe defaults.
156
+ *
157
+ * @param value - The raw value from flag config or rollout logic.
158
+ * @param expectedType - The expected type ('boolean', 'string', 'number', 'json').
159
+ * @returns The value coerced into the expected type.
160
+ */
161
+ export declare function coerceType(value: unknown, expectedType: ExpectedFlagType): FlagEvaluationResult;
162
+ /**
163
+ * @version 1.1.0
164
+ * Evaluate flag with advanced targeting rules (segments + custom rules).
165
+ * Supports variations and rollouts.
166
+ * Order of operations:
167
+ * 1. Flatten context and validate (fail fast for invalid configuration).
168
+ * 2. Iterate through targeting rules in order_index order.
169
+ * 3. For each rule:
170
+ * - Evaluate rule (segment or custom).
171
+ * - If rule matches:
172
+ * - If variation_id is set, return that variation's value.
173
+ * - If rollout_id is set, apply that rollout strategy.
174
+ * 4. If no rules match, return fallback value.
175
+ * @remarks
176
+ * Context usage (raw vs flat):
177
+ * - `context` (raw EvaluationContextT):
178
+ * - Contains identity information: kind, key, user, organization, user_id, etc.
179
+ * - Used for stable rollout keys and identity-based operations.
180
+ * - Use for:
181
+ * - rollout validation (`validateFlagContext`)
182
+ * - rollout hashing (`applyPercentageRollout`, `applyVariantRollout`)
183
+ * - `flatCtx` = `flattenContext(context)`:
184
+ * - Used to make attribute matching easy (e.g., country, plan, user.country).
185
+ * - Strips identity semantics (kind, multi structure).
186
+ * - Use for:
187
+ * - `evaluateRule`
188
+ * - `evaluateTargeting` (rules + segments)
189
+ * @param input - Input parameters including fallback value, expected type, and context.
190
+ * @param targetingRules - Array of targeting rules to evaluate.
191
+ * @param deps - Dependencies including segments, variations, rollouts, and hashing function.
192
+ * @returns Evaluated flag value coerced to the expected type.
193
+ */
194
+ export declare function evaluateFlagWithTargetingRules(input: {
195
+ fallbackValue: unknown;
196
+ expectedType: ExpectedFlagType;
197
+ context: EvaluationContextT;
198
+ }, targetingRules: TargetingRule[], deps: EvaluationDeps): FlagEvaluationResult;
199
+ /**
200
+ * Apply gradual (incremental) rollout strategy.
201
+ * Works for boolean flags only (same restriction as percentage rollout).
202
+ *
203
+ * DESIGN PURPOSE: Time-based progressive feature enablement.
204
+ *
205
+ * Use cases:
206
+ * - Scheduled rollouts: Start at midnight, increase 10% every 6 hours
207
+ * - Automated canary releases: Gradual increase with monitoring windows
208
+ * - Risk mitigation: Slow rollout allows catching issues early
209
+ *
210
+ * HOW IT WORKS:
211
+ * 1. Computes current percentage based on elapsed time (computeCurrentPercentage)
212
+ * 2. If current percentage is 0%, returns fallback (feature off)
213
+ * 3. If current percentage >= 100%, returns true (feature fully on)
214
+ * 4. Otherwise, uses percentage rollout logic with current percentage
215
+ * 5. Hash bucketing ensures same user gets consistent experience across rollout
216
+ *
217
+ * COMPARISON TO OTHER ROLLOUTS:
218
+ * - Percentage: Manual, static percentage set by operator
219
+ * - Gradual: Automated, percentage increases over time
220
+ * - Both use same hash bucketing (same user experience guarantee)
221
+ *
222
+ * WHY BOOLEAN-ONLY?
223
+ * - Same rationale as percentage rollout (semantic clarity, simpler mental model)
224
+ * - "Gradual" implies "progressively enabling", not "switching between variants"
225
+ * - For multi-value gradual experiments, use variant rollout with manual updates
226
+ *
227
+ * NOTE:
228
+ * - User bucketing is stable: once in, always in (until rollout completes)
229
+ * - Current percentage changes over time, but hash bucket stays same
230
+ * - Example: User in bucket 15 sees feature at 20% rollout, keeps it at 30%, 40%, etc.
231
+ */
232
+ export declare function applyGradualRollout(expectedType: ExpectedFlagType, fallbackValue: unknown, rollout: Rollout, context: EvaluationContextT, getHashPercent?: EvaluationDeps['getHashPercent']): FlagEvaluationResult;
233
+ /**
234
+ * Compute the current percentage for a gradual rollout based on elapsed time.
235
+ *
236
+ * DESIGN PURPOSE: Progressive feature rollout over time.
237
+ *
238
+ * Use cases:
239
+ * - Gradually enable a feature: 5% → 10% → 15% → ... → 100% (starts at first increment)
240
+ * - Controlled risk mitigation: Monitor metrics at each stage before continuing
241
+ * - Automated rollout schedule: Set and forget, system handles incremental increases
242
+ *
243
+ * HOW IT WORKS:
244
+ * 1. If start_at is in the future, returns 0% (rollout hasn't started)
245
+ * 2. Once started, begins at 1× increment (not 0%) so traffic is live from hour 0
246
+ * 3. Calculates elapsed time since start_at
247
+ * 4. Each completed interval_hours period adds another increment%
248
+ * 5. Caps at target_percentage
249
+ *
250
+ * EXAMPLE (increment 10%, interval 24h):
251
+ * start_at: 2025-01-01T00:00:00Z
252
+ *
253
+ * Day 0 (2025-01-01): 10% (just started — first increment is live)
254
+ * Day 1 (2025-01-02): 20% (1 full interval completed → 2 steps)
255
+ * Day 2 (2025-01-03): 30% (2 full intervals → 3 steps)
256
+ * ...
257
+ * Day 9 (2025-01-10): 100% (reached target)
258
+ * Day 10+: 100% (capped at target)
259
+ *
260
+ * @param rollout - Gradual rollout configuration
261
+ * @param now - Current timestamp (dayjs object or ISO string)
262
+ * @returns Current percentage (0-100) based on elapsed intervals
263
+ */
264
+ export declare function computeCurrentPercentage(rollout: Rollout, now?: Date | string | number): number;
265
+ /**
266
+ * Apply percentage (simple rollout) strategy.
267
+ *
268
+ * DESIGN DECISION: Percentage rollout is boolean-only.
269
+ *
270
+ * This enforces a clean separation of concerns:
271
+ * - Percentage Rollout (boolean-only): Progressive feature enablement
272
+ * Use case: Gradually enable a feature for 0%, 10%, 50%, 100% of users
273
+ * Result: true (feature on) or fallback value (feature off)
274
+ *
275
+ * - Variant Rollout (multivariate, any type): A/B testing and experiments
276
+ * Use case: Test red vs blue button, experiment with algorithm variants
277
+ * Result: One of N variants based on stable hash bucketing
278
+ *
279
+ * WHY BOOLEAN-ONLY?
280
+ * 1. Semantic clarity: "percentage" implies "on or off", not "variant A/B/C"
281
+ * 2. Prevents confusion: Users won't mistakenly apply percentage to multi-value flags
282
+ * 3. Simpler mental model: Competition convention
283
+ * 4. Fails safely: If someone tries percentage on string/number/json, warns + returns fallback
284
+ *
285
+ * IMPLEMENTATION:
286
+ * - bucket < percentage → true (feature enabled)
287
+ * - bucket >= percentage → fallback value (usually false, feature disabled)
288
+ * - Hash is stable (same user always gets same bucket on same salt)
289
+ *
290
+ * NOTE:
291
+ * For non-boolean flags needing multi-value rollout, use applyVariantRollout()
292
+ * Example: { variants: [{ weight: 50, value: 'blue' }, { weight: 50, value: 'red' }] }
293
+ */
294
+ export declare function applyPercentageRollout(expectedType: ExpectedFlagType, fallbackValue: unknown, rollout: {
295
+ percentage: number;
296
+ salt: string;
297
+ }, context: EvaluationContextT, getHashPercent: (input: string) => number): FlagEvaluationResult;
298
+ /**
299
+ * Apply variant (multivariate) rollout.
300
+ * Works for ANY flag type (boolean, string, number, json).
301
+ *
302
+ * DESIGN PURPOSE: Multi-value A/B testing and experiments.
303
+ *
304
+ * Use cases:
305
+ * - Button color experiment: 50% red, 50% blue
306
+ * - Algorithm variant testing: 33% algorithm-a, 33% algorithm-b, 34% algorithm-c
307
+ * - Configuration experiments: Different JSON payloads for different user buckets
308
+ * - Non-boolean feature variants: Gradual rollout with specific values (not just on/off)
309
+ *
310
+ * HOW IT WORKS:
311
+ * 1. Computes stable hash bucket (0-99) for user + salt
312
+ * 2. Iterates through variants by weight until bucket falls into range
313
+ * 3. Returns matched variant's value, coerced to expected flag type
314
+ *
315
+ * WEIGHT SEMANTICS:
316
+ * - Each variant has a weight (0-100+)
317
+ * - Weights are cumulative: bucket 0-49 → first, 50-99 → second
318
+ * - At WRITE time: validateFlagContext() enforces sum(weights) === 100
319
+ * - At READ time (defensive): If sum < 100, returns fallback; if sum > 100, truncates
320
+ *
321
+ * COMPARISON TO PERCENTAGE ROLLOUT:
322
+ * - Percentage: Boolean-only, simple "on or off"
323
+ * - Variant: Any type, complex "which variant?"
324
+ * - Use percentage for feature flags, variant for experiments
325
+ *
326
+ * NOTE:
327
+ * - Hash is stable per user (same user always matches same variant)
328
+ * - Salt allows independent rollouts of same flag version
329
+ * - Missing key (user_id or kind+key) returns fallback with warning
330
+ * - Weights validated at config save time; evaluator is defensive safety net only
331
+ */
332
+ export declare function applyVariantRollout(expectedType: ExpectedFlagType, fallbackValue: unknown, rollout: Extract<Rollout, {
333
+ strategy: "variant";
334
+ }>, variantById: Record<string, TVariation>, context: EvaluationContextT, getHashPercent: (input: string) => number): FlagEvaluationResult;
335
+ export declare function defaultHash(input: string): number;
336
+ /**
337
+ * Applies the appropriate rollout strategy based on rollout configuration.
338
+ * Centralizes rollout logic and provides better error handling.
339
+ *
340
+ * @param rollout - Rollout configuration
341
+ * @param expectedType - Expected flag type
342
+ * @param fallbackValue - Fallback value if rollout fails
343
+ * @param variationsById - Map of variation IDs to variations
344
+ * @param context - Raw evaluation context (for hashing)
345
+ * @param getHashPercent - Hash function
346
+ * @param ruleId - Rule ID for logging
347
+ * @returns Evaluated flag value
348
+ */
349
+ export declare function applyRolloutStrategy(rollout: Rollout, expectedType: ExpectedFlagType, fallbackValue: unknown, variationsById: Record<string, TVariation>, context: EvaluationContextT, getHashPercent: (input: string) => number, ruleId?: string): FlagEvaluationResult;
350
+ /**
351
+ * Evaluates a custom rule against a flattened context.
352
+ *
353
+ * IMPROVEMENTS OVER ORIGINAL:
354
+ * - Supports both AND and OR logical operators (previously only AND)
355
+ * - Validates conditions array before evaluation
356
+ * - Consistent behavior with segment evaluation
357
+ * - Better error logging
358
+ *
359
+ * Evaluation logic:
360
+ * 1. If conditions is empty → never matches (consistent with segments)
361
+ * 2. If logical_op === "OR" → matches if ANY condition passes
362
+ * 3. Otherwise (AND or undefined) → matches if ALL conditions pass
363
+ *
364
+ * @param rule - Custom rule with conditions and optional logical operator
365
+ * @param context - Flattened evaluation context
366
+ * @returns true if rule matches, false otherwise
367
+ */
368
+ export declare function evaluateCustomRule(rule: {
369
+ conditions: Condition[];
370
+ logical_op?: LogicalOperatorType;
371
+ id?: string;
372
+ }, context: FlatContext): boolean;
373
+ export {};
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Flatten evaluation context — intentional port of FF-EU `utils/context-flatten.ts`
3
+ * (without observed-context hashing helpers).
4
+ *
5
+ * Keep behavior identical to FF-EU:
6
+ * - `addKindLabel=false`: bare keys + `custom.*`
7
+ * - `addKindLabel=true`: `user.key`, `organization.plan`, … (evaluate / SSE path)
8
+ * - Nested `custom` always flattens to bare `custom.<key>` (never `user.custom.<key>`),
9
+ * matching dashboard attribute keys and FF-EU `flattenCustom`.
10
+ * - `kind: 'multi'` only merges `user` + `organization`. Top-level `custom` is not in
11
+ * FF-EU `MultiContext` (`additionalProperties: false`) and is ignored here too.
12
+ */
13
+ export declare function flattenCustom(custom: Record<string, unknown>): Record<string, unknown>;
14
+ /**
15
+ * Flatten a structured EvaluationContext (`kind` present).
16
+ * Returns `{}` if `kind` is missing (callers should pass SSE-flat maps through).
17
+ *
18
+ * Same contract as FF-EU `flattenContext`.
19
+ */
20
+ export declare function flattenContext(ctx: Record<string, any>, addKindLabel?: boolean): Record<string, any>;
21
+ /**
22
+ * SDK convenience for non-schema shapes the client may build before eval:
23
+ * already-flat SSE maps, or nested `{ user, organization, custom }` without `kind`.
24
+ *
25
+ * Structured `kind: user | organization | multi` delegates to `flattenContext(..., true)`
26
+ * (FF-EU evaluate/SSE). Do not invent multi top-level `custom` handling here.
27
+ */
28
+ export declare function flattenEvaluationContext(context: Record<string, unknown> | null | undefined): Record<string, unknown>;
@@ -0,0 +1,21 @@
1
+ import { ASL_KEY_AGREEMENT } from './aslEcdh';
2
+ export type AslHandshakeSuccess = {
3
+ sessionId: string;
4
+ /** Present when ECDH completed (`configSync` handshake). */
5
+ configMacKey?: Uint8Array;
6
+ serverPublicKey?: string;
7
+ salt?: string;
8
+ keyAgreement?: typeof ASL_KEY_AGREEMENT;
9
+ };
10
+ export type AslHandshakeErrorCode = 'ERR_AUTH' | 'ERR_RATE_LIMITED' | 'ERR_INTERNAL' | 'ERR_HANDSHAKE';
11
+ /**
12
+ * POST /auth/asl-handshake.
13
+ * When `withEcdh` is true, sends `clientPublicKey` and derives the config-sync MAC key.
14
+ */
15
+ export declare function performAslHandshake(input: {
16
+ handshakeUrl: string;
17
+ apiKey: string;
18
+ withEcdh?: boolean;
19
+ timeoutMs?: number;
20
+ fetchImpl?: typeof fetch;
21
+ }): Promise<AslHandshakeSuccess>;
@@ -0,0 +1,13 @@
1
+ export { ASL_CLIENT_PUBLIC_KEY_HEX_LENGTH, ASL_HKDF_INFO, ASL_KEY_AGREEMENT, deriveAslMacKey, generateAslClientKeyPair, parsePeerPublicKeyHex, parseSaltHex, wipeKeyMaterial, } from './aslEcdh';
2
+ export type { AslClientKeyPair, AslDerivedMac } from './aslEcdh';
3
+ export { canonicalizeForSigning, isConfigPayloadExpired, signConfigPayload, verifyConfigPayloadSignature, } from './signPayload';
4
+ export { RulesStore, createEmptyRulesState, reduceRules, } from './rulesStore';
5
+ export type { ApplyResult, RulesState } from './rulesStore';
6
+ export type { ConfigSyncPayload, DeltaConfigPayload, DeltasCatchUpPayload, FullConfigPayload, LeasePayload, RulesCacheSnapshot, SdkFlagConfig, SdkSegment, SignableConfigBody, } from './types';
7
+ export { performAslHandshake } from './handshake';
8
+ export type { AslHandshakeSuccess } from './handshake';
9
+ export { evaluateSdkFlag, evaluateAllSdkFlags, coerceType, prepareContextForEvaluator, mapToRecord, } from './evaluateSdkFlag';
10
+ export { evaluateRule, evaluateKillSwitch, evaluateOnVariation, evaluateFlagWithTargetingRules, applyRolloutStrategy, applyPercentageRollout, applyVariantRollout, applyGradualRollout, computeCurrentPercentage, defaultHash, getContextAttribute, } from './flagEvaluator';
11
+ export type { ExpectedFlagType, FlagEvaluationResult, Condition, TargetingRule, Rollout, Variation, Segment, EvaluationDeps, } from './flagEvaluator';
12
+ export { flattenContext, flattenEvaluationContext } from './flattenContext';
13
+ export { stringHash, hashPercent } from './stringHash';