flagmint-js-sdk 2.0.0 → 2.2.1
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/README.md +16 -16
- package/dist/flagmint.cjs.js +4564 -887
- package/dist/flagmint.cjs.js.map +1 -1
- package/dist/flagmint.es.js +4565 -888
- package/dist/flagmint.es.js.map +1 -1
- package/dist/flagmint.umd.js +4564 -887
- package/dist/flagmint.umd.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/sdk/core/client.d.ts +61 -5
- package/dist/sdk/core/config-sync/aslEcdh.d.ts +32 -0
- package/dist/sdk/core/config-sync/evaluateSdkFlag.d.ts +26 -0
- package/dist/sdk/core/config-sync/flagEvaluator.d.ts +373 -0
- package/dist/sdk/core/config-sync/flattenContext.d.ts +28 -0
- package/dist/sdk/core/config-sync/handshake.d.ts +21 -0
- package/dist/sdk/core/config-sync/index.d.ts +13 -0
- package/dist/sdk/core/config-sync/rulesStore.d.ts +60 -0
- package/dist/sdk/core/config-sync/signPayload.d.ts +17 -0
- package/dist/sdk/core/config-sync/stringHash.d.ts +7 -0
- package/dist/sdk/core/config-sync/types.d.ts +72 -0
- package/dist/sdk/core/helpers/applicationEvents.d.ts +12 -1
- package/dist/sdk/core/helpers/cacheHelper.async.d.ts +4 -0
- package/dist/sdk/core/helpers/cacheHelper.d.ts +5 -0
- package/dist/sdk/core/helpers/connectionShare.d.ts +7 -0
- package/dist/sdk/core/helpers/jsonCloneable.d.ts +2 -0
- package/dist/sdk/core/helpers/types.d.ts +7 -0
- package/dist/sdk/core/transports/LongPollingTransport.d.ts +1 -0
- package/dist/sdk/core/transports/SSETransport.d.ts +62 -3
- package/dist/sdk/core/transports/Transport.d.ts +2 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -12,3 +12,5 @@ export { evaluateFlagValue } from './sdk/core/evaluation/evaluateFlagValue';
|
|
|
12
12
|
export { evaluateRollout } from './sdk/core/evaluation/evaluateRollout';
|
|
13
13
|
export { isInSegment } from './sdk/core/evaluation/isInSegment';
|
|
14
14
|
export * from './sdk/core/evaluation/rolloutUtils';
|
|
15
|
+
export { RulesStore, createEmptyRulesState, reduceRules, performAslHandshake, generateAslClientKeyPair, deriveAslMacKey, signConfigPayload, verifyConfigPayloadSignature, canonicalizeForSigning, isConfigPayloadExpired, evaluateSdkFlag, evaluateAllSdkFlags, coerceType, evaluateRule, evaluateFlagWithTargetingRules, applyRolloutStrategy, computeCurrentPercentage, defaultHash, flattenEvaluationContext, stringHash, hashPercent, } from './sdk/core/config-sync';
|
|
16
|
+
export type { SdkFlagConfig, SdkSegment, RulesState, RulesCacheSnapshot, ConfigSyncPayload, LeasePayload, FullConfigPayload, DeltaConfigPayload, DeltasCatchUpPayload, ApplyResult, AslHandshakeSuccess, } from './sdk/core/config-sync';
|
|
@@ -2,6 +2,7 @@ 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
4
|
import { type ConnectionShareOptions } from '../core/helpers/connectionShare';
|
|
5
|
+
import { RulesStore } from '../core/config-sync';
|
|
5
6
|
type TransportMode = 'auto' | 'long-polling' | 'sse';
|
|
6
7
|
export interface FlagClientOptions<C extends Record<string, any> = Record<string, any>> {
|
|
7
8
|
apiKey: string;
|
|
@@ -10,6 +11,14 @@ export interface FlagClientOptions<C extends Record<string, any> = Record<string
|
|
|
10
11
|
persistContext?: boolean;
|
|
11
12
|
transport?: Transport<C, any>;
|
|
12
13
|
transportMode?: TransportMode;
|
|
14
|
+
/**
|
|
15
|
+
* Enable SDK config-sync (local evaluation path).
|
|
16
|
+
* Performs X25519 ECDH on ASL handshake and derives a session MAC key used
|
|
17
|
+
* to verify signed `lease` / `fullConfig` / `delta` payloads.
|
|
18
|
+
* Default false — keeps the legacy evaluated-flags stream.
|
|
19
|
+
* When true, `shareConnection` defaults to false (rules are not shared across tabs yet).
|
|
20
|
+
*/
|
|
21
|
+
configSync?: boolean;
|
|
13
22
|
/**
|
|
14
23
|
* Called when a non-fatal but noteworthy error occurs during or after initialization.
|
|
15
24
|
* The client always resolves ready() regardless — use this to show a degraded/fallback UI.
|
|
@@ -29,8 +38,18 @@ export interface FlagClientOptions<C extends Record<string, any> = Record<string
|
|
|
29
38
|
deferInitialization?: boolean;
|
|
30
39
|
cacheAdapter?: CacheAdapter<C>;
|
|
31
40
|
restEndpoint?: string;
|
|
41
|
+
/**
|
|
42
|
+
* SSE stream base (`…/evaluator/v2/flags`). Defaults to the stream host
|
|
43
|
+
* (`stream.flagmint.com` / `staging-stream…`). EventSource only — context
|
|
44
|
+
* POST uses the API host (see `flagsEndpoint` / `restEndpoint`).
|
|
45
|
+
*/
|
|
32
46
|
sseEndpoint?: string;
|
|
33
|
-
/**
|
|
47
|
+
/**
|
|
48
|
+
* API base for `POST …/context` (`…/evaluator/v2/flags`). Defaults from
|
|
49
|
+
* `restEndpoint`. Override for self-hosted splits that do not match evaluate→v2/flags.
|
|
50
|
+
*/
|
|
51
|
+
flagsEndpoint?: string;
|
|
52
|
+
/** Override the ASL handshake URL. Use with `sseEndpoint` / `restEndpoint` for self-hosted gateways. */
|
|
34
53
|
handshakeEndpoint?: string;
|
|
35
54
|
debugLog?: boolean;
|
|
36
55
|
env?: string;
|
|
@@ -71,10 +90,16 @@ export declare class FlagClient<T = unknown, C extends Record<string, any> = Rec
|
|
|
71
90
|
private eventsEndpoint;
|
|
72
91
|
private aslHandshakeUrl;
|
|
73
92
|
private sseEndpoint;
|
|
93
|
+
/** API host base for POST /context (stream host may be SSE-only). */
|
|
94
|
+
private flagsApiEndpoint;
|
|
74
95
|
private eventQueue;
|
|
75
96
|
private eventFlushTimer;
|
|
97
|
+
/** Coalesced getFlag hits waiting for EVAL_REPORT_FLUSH_MS. */
|
|
98
|
+
private evaluationReportQueue;
|
|
99
|
+
private evaluationReportTimer;
|
|
76
100
|
/** null = server has not sent a map yet; send and let ingest drop. */
|
|
77
101
|
private analyticsByFlag;
|
|
102
|
+
private contextUpdateSeq;
|
|
78
103
|
private readyPromise;
|
|
79
104
|
private resolveReady;
|
|
80
105
|
private rejectReady;
|
|
@@ -90,6 +115,10 @@ export declare class FlagClient<T = unknown, C extends Record<string, any> = Rec
|
|
|
90
115
|
private subscribers;
|
|
91
116
|
private shareHub;
|
|
92
117
|
private shareConnection;
|
|
118
|
+
private configSync;
|
|
119
|
+
private rulesStore;
|
|
120
|
+
/** Avoid stacking reconnects when many getFlag calls hit an expired lease. */
|
|
121
|
+
private leaseRenewInFlight;
|
|
93
122
|
/**
|
|
94
123
|
* Creates a new FlagClient instance.
|
|
95
124
|
* @param options - Configuration options for the client.
|
|
@@ -151,12 +180,18 @@ export declare class FlagClient<T = unknown, C extends Record<string, any> = Rec
|
|
|
151
180
|
subscribe(callback: FlagUpdateCallback<T>): () => void;
|
|
152
181
|
/**
|
|
153
182
|
* Get all flags.
|
|
183
|
+
* Config-sync: if the lease expired, fail-closed to defaults and reconnect.
|
|
154
184
|
*/
|
|
155
185
|
getFlags(): FeatureFlags<T>;
|
|
156
186
|
/**
|
|
157
187
|
* Get a single flag value.
|
|
188
|
+
* When analytics is on for the flag, queues a debounced call-site evaluation
|
|
189
|
+
* report (dashboard Evaluations / unique users). Does not consume billing quota.
|
|
190
|
+
* Config-sync: expired lease → defaults only, then drop/reconnect (24h renew).
|
|
158
191
|
*/
|
|
159
192
|
getFlag<K extends keyof FeatureFlags<T>>(key: K, fallback?: FeatureFlags<T>[K]): FeatureFlags<T>[K];
|
|
193
|
+
private queueEvaluationReport;
|
|
194
|
+
private flushEvaluationReports;
|
|
160
195
|
/**
|
|
161
196
|
* Report an application error that happened after this flag was served.
|
|
162
197
|
* Fire-and-forget: events are batched and never throw.
|
|
@@ -176,6 +211,7 @@ export declare class FlagClient<T = unknown, C extends Record<string, any> = Rec
|
|
|
176
211
|
isAnalyticsEnabled(flagKey: string): boolean | undefined;
|
|
177
212
|
/**
|
|
178
213
|
* Update the evaluation context.
|
|
214
|
+
* Config-sync: renews the lease stream first if expired (still evaluates locally).
|
|
179
215
|
*/
|
|
180
216
|
updateContext(context: C): Promise<void>;
|
|
181
217
|
/**
|
|
@@ -191,8 +227,8 @@ export declare class FlagClient<T = unknown, C extends Record<string, any> = Rec
|
|
|
191
227
|
* become available (or until the specified timeout expires) before awaiting
|
|
192
228
|
* completion of the underlying transport initialization.
|
|
193
229
|
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
230
|
+
* Transport and connection failures do not reject this promise. The client reports them through the `onError` option and resolves anyway, so the
|
|
231
|
+
* application can render a fallback or degraded UI.
|
|
196
232
|
*
|
|
197
233
|
* @param {number} [timeoutMs=3000] - The maximum amount of time, in
|
|
198
234
|
* milliseconds, to wait for the initial feature flag payload before
|
|
@@ -201,8 +237,7 @@ export declare class FlagClient<T = unknown, C extends Record<string, any> = Rec
|
|
|
201
237
|
* @returns {Promise<void>} A promise that resolves when the client is fully
|
|
202
238
|
* initialized and ready to evaluate feature flags.
|
|
203
239
|
*
|
|
204
|
-
*
|
|
205
|
-
* such as authentication, rate limiting, or connection errors.
|
|
240
|
+
*
|
|
206
241
|
*/
|
|
207
242
|
ready(timeoutMs?: number): Promise<void>;
|
|
208
243
|
/**
|
|
@@ -232,6 +267,15 @@ export declare class FlagClient<T = unknown, C extends Record<string, any> = Rec
|
|
|
232
267
|
private updateAnalytics;
|
|
233
268
|
private queueApplicationEvent;
|
|
234
269
|
private flushApplicationEvents;
|
|
270
|
+
/**
|
|
271
|
+
* If a previously-ready config-sync lease just crossed expiresAt: fail-closed
|
|
272
|
+
* to defaults, persist, notify once, and reconnect for fullConfig.
|
|
273
|
+
* Bootstrapping or already-expired stores return without side effects so
|
|
274
|
+
* getFlag/getFlags cannot re-enter via subscriber updates.
|
|
275
|
+
*
|
|
276
|
+
* @param now Wall clock for expiry check
|
|
277
|
+
*/
|
|
278
|
+
private ensureConfigSyncLeaseFresh;
|
|
235
279
|
/**
|
|
236
280
|
* Performs the initial authentication handshake with the Flagmint server
|
|
237
281
|
* to obtain a fresh, single-use session identifier.
|
|
@@ -253,5 +297,17 @@ export declare class FlagClient<T = unknown, C extends Record<string, any> = Rec
|
|
|
253
297
|
* @throws {Error} If the handshake response does not contain a valid session ID.
|
|
254
298
|
*/
|
|
255
299
|
private fetchFreshSessionId;
|
|
300
|
+
/** Config-sync rules store (null when `configSync` is false). */
|
|
301
|
+
getRulesStore(): RulesStore | null;
|
|
302
|
+
/**
|
|
303
|
+
* Restore rules from localCache before the stream opens.
|
|
304
|
+
* Fresh lease → reconnect with sinceVersion; expired → fullConfig.
|
|
305
|
+
*/
|
|
306
|
+
private hydrateRulesFromCache;
|
|
307
|
+
/** Persist rules after a successful (or expiry/gap) apply so reconnect can catch up. */
|
|
308
|
+
private persistRulesSnapshot;
|
|
309
|
+
private handleConfigSyncEvent;
|
|
310
|
+
private evaluateFromRulesStore;
|
|
311
|
+
private analyticsMapFromRulesStore;
|
|
256
312
|
}
|
|
257
313
|
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';
|