flagmint-js-sdk 2.2.2 → 2.2.3-rc-2

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.
@@ -16,7 +16,8 @@ export interface FlagClientOptions<C extends Record<string, any> = Record<string
16
16
  * Performs X25519 ECDH on ASL handshake and derives a session MAC key used
17
17
  * to verify signed `lease` / `fullConfig` / `delta` payloads.
18
18
  * Default false — keeps the legacy evaluated-flags stream.
19
- * When true, `shareConnection` defaults to false (rules are not shared across tabs yet).
19
+ * When true and `shareConnection` is on, tabs share **rules** only (one SSE);
20
+ * each document keeps its own context and evaluates locally.
20
21
  */
21
22
  configSync?: boolean;
22
23
  /**
@@ -68,6 +69,8 @@ export interface FlagClientOptions<C extends Record<string, any> = Record<string
68
69
  * leader and forward updateContext() to it. Do not enable this when two clients
69
70
  * must evaluate different contexts at the same time on one page, or when the
70
71
  * origin hosts untrusted documents that should not steer or read this stream.
72
+ *
73
+ * With `configSync`, sharing uses rules-only mode (not evaluated maps / context RPC).
71
74
  */
72
75
  shareConnection?: boolean;
73
76
  /**
@@ -133,7 +136,20 @@ export declare class FlagClient<T = unknown, C extends Record<string, any> = Rec
133
136
  * @returns true when this document is a follower and should skip opening a stream.
134
137
  */
135
138
  private joinConnectionShare;
139
+ /**
140
+ * Leader: wire context RPC for evaluated share only.
141
+ * Rules share never forwards follower context onto the stream.
142
+ */
136
143
  private attachShareLeader;
144
+ /**
145
+ * Follower hooks for rules-only share: hydrate rules locally and evaluate
146
+ * with this document's context (never the leader's).
147
+ */
148
+ private attachRulesShareFollower;
149
+ /**
150
+ * Leader: push the right snapshot type to followers after flags/rules change.
151
+ */
152
+ private broadcastShareSnapshot;
137
153
  /**
138
154
  * Creates and initializes the transport layer used to receive feature flags
139
155
  * updates from the Flagmint platform.
@@ -7,7 +7,7 @@ export type { ConfigSyncPayload, DeltaConfigPayload, DeltasCatchUpPayload, FullC
7
7
  export { performAslHandshake } from './handshake';
8
8
  export type { AslHandshakeSuccess } from './handshake';
9
9
  export { evaluateSdkFlag, evaluateAllSdkFlags, coerceType, prepareContextForEvaluator, prepareContextForLocalEval, mapToRecord, } from './evaluateSdkFlag';
10
- export { PII_VALUE_ENCODING, normalizePiiString, hashPiiValue, isPiiAttribute, payloadHasExplicitPiiFields, piiWireOptionsFromPayload, resolvePiiWireForDelta, applyPiiHashToFlatContext, } from './piiHash';
10
+ export { PII_VALUE_ENCODING, normalizePiiString, hashPiiValue, isPiiAttribute, payloadHasExplicitPiiFields, hasExplicitInvalidPiiEncoding, hasMalformedPiiAttributes, payloadRequiresUsablePiiWire, isExplicitPiiRequirementCleared, piiAttributeKeysFromPayload, piiWireOptionsFromPayload, isPiiWireBroken, resolvePiiWireForDelta, applyPiiHashToFlatContext, } from './piiHash';
11
11
  export type { PiiWireOptions, ApplyPiiHashMode } from './piiHash';
12
12
  export { evaluateRule, evaluateKillSwitch, evaluateOnVariation, evaluateFlagWithTargetingRules, applyRolloutStrategy, applyPercentageRollout, applyVariantRollout, applyGradualRollout, computeCurrentPercentage, defaultHash, getContextAttribute, } from './flagEvaluator';
13
13
  export type { ExpectedFlagType, FlagEvaluationResult, Condition, TargetingRule, Rollout, Variation, Segment, EvaluationDeps, } from './flagEvaluator';
@@ -53,6 +53,40 @@ export type PiiWirePayloadFields = {
53
53
  * @param payload Signed fullConfig / delta fields
54
54
  */
55
55
  export declare function payloadHasExplicitPiiFields(payload: PiiWirePayloadFields | null | undefined): boolean;
56
+ /**
57
+ * String attribute keys listed on a PII wire payload (ignores non-strings).
58
+ *
59
+ * @param payload Signed fullConfig / delta fields
60
+ */
61
+ export declare function piiAttributeKeysFromPayload(payload: PiiWirePayloadFields | null | undefined): string[];
62
+ /**
63
+ * True when `piiAttributes` is present but not a clean list of non-empty strings.
64
+ * e.g. `[null]`, `['']`, or a non-array — must not count as "remove all PII".
65
+ *
66
+ * @param payload Signed fullConfig / delta fields
67
+ */
68
+ export declare function hasMalformedPiiAttributes(payload: PiiWirePayloadFields | null | undefined): boolean;
69
+ /**
70
+ * True when `piiValueEncoding` is present but is not usable `hmac-v1`.
71
+ * Distinguishes omitted encoding (OK — default hmac-v1) from e.g. `123` / `hmac-v2`.
72
+ *
73
+ * @param payload Signed fullConfig / delta fields
74
+ */
75
+ export declare function hasExplicitInvalidPiiEncoding(payload: PiiWirePayloadFields | null | undefined): boolean;
76
+ /**
77
+ * True when the payload claims PII hashing is in play and the client must have
78
+ * usable salt + `hmac-v1` encoding (otherwise local eval must fail closed).
79
+ *
80
+ * @param payload Signed fullConfig / delta fields
81
+ */
82
+ export declare function payloadRequiresUsablePiiWire(payload: PiiWirePayloadFields | null | undefined): boolean;
83
+ /**
84
+ * True when the payload explicitly clears the PII attribute list.
85
+ * Only a literal empty array `[]` counts — `[null]` / `['']` are malformed, not removal.
86
+ *
87
+ * @param payload Signed fullConfig / delta fields
88
+ */
89
+ export declare function isExplicitPiiRequirementCleared(payload: PiiWirePayloadFields | null | undefined): boolean;
56
90
  /**
57
91
  * Parse PII wire options from a config-sync payload body.
58
92
  *
@@ -60,6 +94,18 @@ export declare function payloadHasExplicitPiiFields(payload: PiiWirePayloadField
60
94
  * @returns Options when salt + encoding are usable; otherwise null
61
95
  */
62
96
  export declare function piiWireOptionsFromPayload(payload: PiiWirePayloadFields | null | undefined): PiiWireOptions | null;
97
+ /**
98
+ * Whether resolved PII options are missing while the payload (or prior state)
99
+ * still requires hashing — local eval must use defaults only.
100
+ *
101
+ * Explicit `piiAttributes: []` (literal empty array) clears the requirement
102
+ * even if salt is empty — evaluation may proceed without hashing.
103
+ *
104
+ * @param payload Payload that was just applied (full or merged delta fields)
105
+ * @param resolved Result of {@link piiWireOptionsFromPayload} / delta resolve
106
+ * @param previousPriorHadPiiAttrs Whether the store previously had PII attribute keys
107
+ */
108
+ export declare function isPiiWireBroken(payload: PiiWirePayloadFields | null | undefined, resolved: PiiWireOptions | null, previousPriorHadPiiAttrs?: boolean): boolean;
63
109
  /**
64
110
  * Resolve PII wire options for a delta: keep prior when all PII fields are
65
111
  * absent; otherwise parse after merging omitted fields from `previous`.
@@ -11,6 +11,11 @@ export type RulesState = {
11
11
  needsFullConfig: boolean;
12
12
  /** PII hash options from the last fullConfig/delta (null when absent). */
13
13
  piiWire: PiiWireOptions | null;
14
+ /**
15
+ * True when hashed PII rules are expected but salt/encoding is unusable.
16
+ * Fail closed (defaults only) until a good fullConfig arrives.
17
+ */
18
+ piiWireBroken: boolean;
14
19
  };
15
20
  export type ApplyResult = {
16
21
  ok: true;
@@ -36,7 +41,8 @@ export declare class RulesStore {
36
41
  getMacKey(): Uint8Array | null;
37
42
  getState(): RulesState;
38
43
  /**
39
- * Fail-closed readiness: lease not expired and store marked ready.
44
+ * Fail-closed readiness: lease not expired, store marked ready, and PII
45
+ * wire metadata usable when hashing is required.
40
46
  */
41
47
  isReady(now?: number): boolean;
42
48
  /**
@@ -79,4 +79,9 @@ export type RulesCacheSnapshot = {
79
79
  piiAttributes?: string[];
80
80
  piiSalt?: string;
81
81
  piiValueEncoding?: string;
82
+ /**
83
+ * When true, hydrate must stay fail-closed even if salt/attrs were omitted
84
+ * from the snapshot (broken wire with `piiWire === null`).
85
+ */
86
+ piiWireBroken?: boolean;
82
87
  };
@@ -8,34 +8,37 @@
8
8
  *
9
9
  * This hub elects a single **leader** per API key:
10
10
  *
11
- * - **Leader** holds the real transport (handshake + EventSource). It broadcasts
12
- * flag snapshots and errors over {@link BroadcastChannel}.
13
- * - **Followers** skip handshake and EventSource. They apply the leader's flags
14
- * and forward `updateContext()` to the leader, which runs it on the shared
15
- * stream.
11
+ * - **Leader** holds the real transport (handshake + EventSource).
12
+ * - **Followers** skip handshake and EventSource.
16
13
  *
17
- * Leadership is stored in `localStorage` (`flagmint_share_lock:<apiKey>`) with a
18
- * short TTL and heartbeat. `destroy()` and a `pagehide` / `beforeunload` listener
19
- * post `bye` and release the lock so another member can take over immediately.
20
- * If the document is killed without those events, followers wait until the lock
21
- * TTL expires.
14
+ * Two modes (separate BroadcastChannel / lock keys so they never mix):
22
15
  *
23
- * Sharing is a **browser** feature. Node cluster workers each need their own
24
- * stream — {@link isConnectionSharingAvailable} is false there. Do not share
25
- * when two clients on one page must keep different evaluation contexts at the
26
- * same time; `/context` is per connection, so the last write wins.
16
+ * - **`evaluated`** (server eval, default): leader broadcasts evaluated flag
17
+ * maps; followers forward `updateContext` onto the leader stream (last write
18
+ * wins). Same behavior as before.
19
+ * - **`rules`** (config sync): leader broadcasts verified **rules snapshots**;
20
+ * each tab keeps its own context and evaluates locally. Followers do **not**
21
+ * forward context to the leader.
27
22
  *
28
- * Trust: `BroadcastChannel` and `localStorage` are same-origin. Any document
29
- * on this origin (including a third-party iframe you host there) can join the
30
- * group, POST a context through the leader's authenticated stream, and read
31
- * the flags broadcast back. That is expected among trusted siblings. If the
32
- * origin serves untrusted content, set `shareConnection: false`.
23
+ * Leadership is stored in `localStorage` with a short TTL and heartbeat.
24
+ * `destroy()` and a `pagehide` / `beforeunload` listener post `bye` and release
25
+ * the lock so another member can take over immediately.
26
+ *
27
+ * Trust: `BroadcastChannel` and `localStorage` are same-origin. If the origin
28
+ * serves untrusted content, set `shareConnection: false`.
33
29
  *
34
30
  * @module connectionShare
35
31
  */
36
32
  import type { Transport } from '../../core/transports/Transport';
33
+ import type { RulesCacheSnapshot } from '../../core/config-sync/types';
37
34
  /** Role this document plays in the share group for a given API key. */
38
35
  export type ShareRole = 'leader' | 'follower';
36
+ /**
37
+ * What the leader shares with followers.
38
+ * - `evaluated` — classic server-eval flag maps + context RPC
39
+ * - `rules` — config-sync rules snapshots; local eval per tab
40
+ */
41
+ export type ConnectionShareMode = 'evaluated' | 'rules';
39
42
  /**
40
43
  * Persistence used for the leadership lock.
41
44
  * Defaults to `localStorage` so sibling iframes on the same origin can compete.
@@ -61,6 +64,11 @@ export interface ShareChannel {
61
64
  * `localStorage`, and `setInterval`.
62
65
  */
63
66
  export interface ConnectionShareOptions {
67
+ /**
68
+ * `evaluated` (default): server-eval share.
69
+ * `rules`: config-sync share — rules only, per-tab context.
70
+ */
71
+ mode?: ConnectionShareMode;
64
72
  /** Override `new BroadcastChannel(name)` (tests). */
65
73
  channelFactory?: (name: string) => ShareChannel;
66
74
  /** Override `localStorage` for the leadership lock (tests). */
@@ -99,11 +107,18 @@ export declare class ConnectionShareHub<C = Record<string, unknown>, T = unknown
99
107
  private watchId;
100
108
  private closed;
101
109
  private latestFlags;
110
+ private latestRules;
102
111
  private flagsWaiters;
112
+ private rulesWaiters;
103
113
  private flagsTimeouts;
114
+ private rulesTimeouts;
104
115
  private contextWaiters;
105
116
  private onPromoteCallback?;
106
117
  private leaderContextHandler?;
118
+ /** Rules mode: apply leader snapshot → return flags evaluated for this tab. */
119
+ private followerRulesApply?;
120
+ /** Rules mode: re-evaluate after local `updateContext` (no leader RPC). */
121
+ private followerLocalEval?;
107
122
  private followerFlagsCallback?;
108
123
  private followerErrorCallback?;
109
124
  private readonly now;
@@ -115,6 +130,7 @@ export declare class ConnectionShareHub<C = Record<string, unknown>, T = unknown
115
130
  private unloadHandler?;
116
131
  private readonly lockKey;
117
132
  private readonly channelName;
133
+ private readonly mode;
118
134
  /**
119
135
  * @param apiKey Share groups are keyed by API key. Distinct keys never share.
120
136
  * @param options Optional test doubles for channel, storage, and timers.
@@ -122,6 +138,8 @@ export declare class ConnectionShareHub<C = Record<string, unknown>, T = unknown
122
138
  constructor(apiKey: string, options?: ConnectionShareOptions);
123
139
  /** `leader` if this document holds the lock; otherwise `follower`. */
124
140
  get currentRole(): ShareRole;
141
+ /** `evaluated` (server eval) or `rules` (config sync). */
142
+ get shareMode(): ConnectionShareMode;
125
143
  /**
126
144
  * Join the share group: compete for the lock, subscribe to the channel.
127
145
  *
@@ -146,16 +164,43 @@ export declare class ConnectionShareHub<C = Record<string, unknown>, T = unknown
146
164
  setLeaderContextHandler(handler: (context: C, options?: {
147
165
  persist?: boolean;
148
166
  }) => Promise<Record<string, T>>): void;
167
+ /**
168
+ * Rules-mode follower: hydrate local rules from the leader snapshot and
169
+ * return flags evaluated for **this** document's context.
170
+ *
171
+ * @param handler Apply snapshot + local eval → flag map
172
+ */
173
+ setFollowerRulesApply(handler: (payload: {
174
+ snapshot: RulesCacheSnapshot;
175
+ analytics?: Record<string, boolean>;
176
+ }) => Record<string, T>): void;
177
+ /**
178
+ * Rules-mode follower: re-evaluate after local `updateContext` (no RPC).
179
+ *
180
+ * @param handler Context → flag map using this tab's rules store
181
+ */
182
+ setFollowerLocalEval(handler: (context: C) => Record<string, T>): void;
149
183
  /**
150
184
  * Leader-only: publish the current evaluated flags to all followers.
151
- * No-op on followers (avoids echo loops).
185
+ * No-op on followers (avoids echo loops). Evaluated mode only.
186
+ *
187
+ * @param flags Evaluated flag map from the leader stream
152
188
  */
153
189
  broadcastFlags(flags: Record<string, T>): void;
190
+ /**
191
+ * Leader-only (rules mode): publish a verified rules snapshot.
192
+ * Followers hydrate and evaluate with their own context.
193
+ *
194
+ * @param snapshot Rules store snapshot (MAC already verified on the leader)
195
+ * @param analytics Optional per-flag analytics map from rules
196
+ */
197
+ broadcastRules(snapshot: RulesCacheSnapshot, analytics?: Record<string, boolean>): void;
154
198
  /** Leader-only: mirror a transport `onError` (auth, quota) to followers. */
155
199
  broadcastError(error: Error): void;
156
200
  /**
157
- * Transport used by follower FlagClients. `init` waits for the leader
158
- * snapshot; `fetchFlags` RPCs `updateContext` through the leader.
201
+ * Transport used by follower FlagClients.
202
+ * - Evaluated: `init` waits for flags; `fetchFlags` RPCs through the leader.
203
+ * - Rules: `init` waits for a rules snapshot; `fetchFlags` evaluates locally.
159
204
  * `destroy` is a no-op — {@link ConnectionShareHub.destroy} owns teardown.
160
205
  */
161
206
  createFollowerTransport(): Transport<C, T>;
@@ -196,6 +241,12 @@ export declare class ConnectionShareHub<C = Record<string, unknown>, T = unknown
196
241
  * Used by follower `init()`.
197
242
  */
198
243
  private waitForFlags;
244
+ /**
245
+ * Block until the leader has sent a `rules` snapshot, or {@link FOLLOWER_WAIT_MS}.
246
+ *
247
+ * @returns Latest rules payload from the leader
248
+ */
249
+ private waitForRules;
199
250
  /**
200
251
  * Follower `fetchFlags`: ask the leader to POST `/context` and wait for
201
252
  * `context-result`. Leaders run the handler locally.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flagmint-js-sdk",
3
- "version": "2.2.2",
3
+ "version": "2.2.3-rc-2",
4
4
  "description": "Framework-agnostic JavaScript SDK for managing feature flags in Flagmint applications.",
5
5
  "author": "Flagmint Team",
6
6
  "license": "BSD 3-Clause",