@shipeasy/sdk 2.0.1 → 2.0.3

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.
@@ -118,6 +118,15 @@ declare function getShipeasyClient(): FlagsClientBrowser | null;
118
118
  * Not part of the documented surface; production code should never call this.
119
119
  */
120
120
  declare function _resetShipeasyForTests(): void;
121
+ interface BootstrapPayload {
122
+ flags: Record<string, boolean>;
123
+ configs: Record<string, unknown>;
124
+ experiments: Record<string, {
125
+ inExperiment: boolean;
126
+ group: string;
127
+ params: Record<string, unknown>;
128
+ }>;
129
+ }
121
130
  /**
122
131
  * Universal flags facade. Methods return safe defaults when the singleton
123
132
  * hasn't been configured yet (false / undefined / `notIn` experiment), so
@@ -127,8 +136,11 @@ declare const flags: {
127
136
  configure(opts: FlagsClientBrowserOptions): void;
128
137
  identify(user: User): Promise<void>;
129
138
  /**
130
- * Read a feature gate. Returns false until FlagsBoundary mounts (SSR-safe).
131
- * After mount, URL overrides (?se_ks_*) apply even without a configured client.
139
+ * Read a feature gate.
140
+ * Priority: bootstrap → CDN/URL-override (post-mount) false.
141
+ * Bootstrap is safe before mount because the server rendered with the same values.
142
+ * Everything else gates on _mountedAndReady to prevent hydration mismatches on
143
+ * force-static pages where SSR has no flag data.
132
144
  */
133
145
  get(name: string): boolean;
134
146
  getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
@@ -138,7 +150,11 @@ declare const flags: {
138
150
  /**
139
151
  * Called by FlagsBoundary after React hydration to unlock flag reads.
140
152
  * Dispatches se:override:change so subscribers (FlagsBoundary) re-render
141
- * once with real values — URL overrides and server-evaluated flags.
153
+ * once with real values — URL overrides and CDN-loaded flags.
154
+ *
155
+ * Always dispatches even if already mounted: in React hydration-recovery
156
+ * renders the latch is already true so the early-return guard would swallow
157
+ * the event, leaving the re-mounted subtree stuck with stale (empty) values.
142
158
  */
143
159
  notifyMounted(): void;
144
160
  /** Subscribe for change notifications (identify/override). Used by framework adapters. */
@@ -190,4 +206,4 @@ declare const i18n: {
190
206
  onUpdate(cb: () => void): () => void;
191
207
  };
192
208
 
193
- export { type ExperimentResult, FlagsClientBrowser, type FlagsClientBrowserEnv, type FlagsClientBrowserOptions, LABEL_MARKER_END, LABEL_MARKER_RE, LABEL_MARKER_SEP, LABEL_MARKER_START, type LabelAttrs, type ShipeasySdkBridge, type User, _resetShipeasyForTests, attachDevtools, configureShipeasy, encodeLabelMarker, flags, getShipeasyClient, i18n, isDevtoolsRequested, labelAttrs, loadDevtools, readConfigOverride, readExpOverride, readGateOverride, version };
209
+ export { type BootstrapPayload, type ExperimentResult, FlagsClientBrowser, type FlagsClientBrowserEnv, type FlagsClientBrowserOptions, LABEL_MARKER_END, LABEL_MARKER_RE, LABEL_MARKER_SEP, LABEL_MARKER_START, type LabelAttrs, type ShipeasySdkBridge, type User, _resetShipeasyForTests, attachDevtools, configureShipeasy, encodeLabelMarker, flags, getShipeasyClient, i18n, isDevtoolsRequested, labelAttrs, loadDevtools, readConfigOverride, readExpOverride, readGateOverride, version };
@@ -118,6 +118,15 @@ declare function getShipeasyClient(): FlagsClientBrowser | null;
118
118
  * Not part of the documented surface; production code should never call this.
119
119
  */
120
120
  declare function _resetShipeasyForTests(): void;
121
+ interface BootstrapPayload {
122
+ flags: Record<string, boolean>;
123
+ configs: Record<string, unknown>;
124
+ experiments: Record<string, {
125
+ inExperiment: boolean;
126
+ group: string;
127
+ params: Record<string, unknown>;
128
+ }>;
129
+ }
121
130
  /**
122
131
  * Universal flags facade. Methods return safe defaults when the singleton
123
132
  * hasn't been configured yet (false / undefined / `notIn` experiment), so
@@ -127,8 +136,11 @@ declare const flags: {
127
136
  configure(opts: FlagsClientBrowserOptions): void;
128
137
  identify(user: User): Promise<void>;
129
138
  /**
130
- * Read a feature gate. Returns false until FlagsBoundary mounts (SSR-safe).
131
- * After mount, URL overrides (?se_ks_*) apply even without a configured client.
139
+ * Read a feature gate.
140
+ * Priority: bootstrap → CDN/URL-override (post-mount) false.
141
+ * Bootstrap is safe before mount because the server rendered with the same values.
142
+ * Everything else gates on _mountedAndReady to prevent hydration mismatches on
143
+ * force-static pages where SSR has no flag data.
132
144
  */
133
145
  get(name: string): boolean;
134
146
  getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
@@ -138,7 +150,11 @@ declare const flags: {
138
150
  /**
139
151
  * Called by FlagsBoundary after React hydration to unlock flag reads.
140
152
  * Dispatches se:override:change so subscribers (FlagsBoundary) re-render
141
- * once with real values — URL overrides and server-evaluated flags.
153
+ * once with real values — URL overrides and CDN-loaded flags.
154
+ *
155
+ * Always dispatches even if already mounted: in React hydration-recovery
156
+ * renders the latch is already true so the early-return guard would swallow
157
+ * the event, leaving the re-mounted subtree stuck with stale (empty) values.
142
158
  */
143
159
  notifyMounted(): void;
144
160
  /** Subscribe for change notifications (identify/override). Used by framework adapters. */
@@ -190,4 +206,4 @@ declare const i18n: {
190
206
  onUpdate(cb: () => void): () => void;
191
207
  };
192
208
 
193
- export { type ExperimentResult, FlagsClientBrowser, type FlagsClientBrowserEnv, type FlagsClientBrowserOptions, LABEL_MARKER_END, LABEL_MARKER_RE, LABEL_MARKER_SEP, LABEL_MARKER_START, type LabelAttrs, type ShipeasySdkBridge, type User, _resetShipeasyForTests, attachDevtools, configureShipeasy, encodeLabelMarker, flags, getShipeasyClient, i18n, isDevtoolsRequested, labelAttrs, loadDevtools, readConfigOverride, readExpOverride, readGateOverride, version };
209
+ export { type BootstrapPayload, type ExperimentResult, FlagsClientBrowser, type FlagsClientBrowserEnv, type FlagsClientBrowserOptions, LABEL_MARKER_END, LABEL_MARKER_RE, LABEL_MARKER_SEP, LABEL_MARKER_START, type LabelAttrs, type ShipeasySdkBridge, type User, _resetShipeasyForTests, attachDevtools, configureShipeasy, encodeLabelMarker, flags, getShipeasyClient, i18n, isDevtoolsRequested, labelAttrs, loadDevtools, readConfigOverride, readExpOverride, readGateOverride, version };
@@ -659,6 +659,10 @@ function _resetShipeasyForTests() {
659
659
  _client?.destroy();
660
660
  _client = null;
661
661
  }
662
+ function getBootstrap() {
663
+ if (typeof window === "undefined") return null;
664
+ return window.__SE_BOOTSTRAP ?? null;
665
+ }
662
666
  var _mountedAndReady = false;
663
667
  var _standaloneListeners = /* @__PURE__ */ new Set();
664
668
  var _standaloneOverrideWired = false;
@@ -681,15 +685,30 @@ var flags = {
681
685
  return _client.identify(user);
682
686
  },
683
687
  /**
684
- * Read a feature gate. Returns false until FlagsBoundary mounts (SSR-safe).
685
- * After mount, URL overrides (?se_ks_*) apply even without a configured client.
688
+ * Read a feature gate.
689
+ * Priority: bootstrap → CDN/URL-override (post-mount) false.
690
+ * Bootstrap is safe before mount because the server rendered with the same values.
691
+ * Everything else gates on _mountedAndReady to prevent hydration mismatches on
692
+ * force-static pages where SSR has no flag data.
686
693
  */
687
694
  get(name) {
695
+ const bs = getBootstrap();
696
+ if (bs !== null && name in bs.flags) return bs.flags[name];
688
697
  if (!_mountedAndReady) return false;
689
698
  if (_client) return _client.getFlag(name);
690
699
  return readGateOverride(name) ?? false;
691
700
  },
692
701
  getConfig(name, decode) {
702
+ const bs = getBootstrap();
703
+ if (bs !== null && name in bs.configs) {
704
+ const raw = bs.configs[name];
705
+ if (!decode) return raw;
706
+ try {
707
+ return decode(raw);
708
+ } catch {
709
+ return void 0;
710
+ }
711
+ }
693
712
  if (!_mountedAndReady) return void 0;
694
713
  if (_client) return _client.getConfig(name, decode);
695
714
  const ov = readConfigOverride(name);
@@ -717,10 +736,13 @@ var flags = {
717
736
  /**
718
737
  * Called by FlagsBoundary after React hydration to unlock flag reads.
719
738
  * Dispatches se:override:change so subscribers (FlagsBoundary) re-render
720
- * once with real values — URL overrides and server-evaluated flags.
739
+ * once with real values — URL overrides and CDN-loaded flags.
740
+ *
741
+ * Always dispatches even if already mounted: in React hydration-recovery
742
+ * renders the latch is already true so the early-return guard would swallow
743
+ * the event, leaving the re-mounted subtree stuck with stale (empty) values.
721
744
  */
722
745
  notifyMounted() {
723
- if (_mountedAndReady) return;
724
746
  _mountedAndReady = true;
725
747
  if (typeof window !== "undefined") {
726
748
  window.dispatchEvent(new CustomEvent("se:override:change"));
@@ -617,6 +617,10 @@ function _resetShipeasyForTests() {
617
617
  _client?.destroy();
618
618
  _client = null;
619
619
  }
620
+ function getBootstrap() {
621
+ if (typeof window === "undefined") return null;
622
+ return window.__SE_BOOTSTRAP ?? null;
623
+ }
620
624
  var _mountedAndReady = false;
621
625
  var _standaloneListeners = /* @__PURE__ */ new Set();
622
626
  var _standaloneOverrideWired = false;
@@ -639,15 +643,30 @@ var flags = {
639
643
  return _client.identify(user);
640
644
  },
641
645
  /**
642
- * Read a feature gate. Returns false until FlagsBoundary mounts (SSR-safe).
643
- * After mount, URL overrides (?se_ks_*) apply even without a configured client.
646
+ * Read a feature gate.
647
+ * Priority: bootstrap → CDN/URL-override (post-mount) false.
648
+ * Bootstrap is safe before mount because the server rendered with the same values.
649
+ * Everything else gates on _mountedAndReady to prevent hydration mismatches on
650
+ * force-static pages where SSR has no flag data.
644
651
  */
645
652
  get(name) {
653
+ const bs = getBootstrap();
654
+ if (bs !== null && name in bs.flags) return bs.flags[name];
646
655
  if (!_mountedAndReady) return false;
647
656
  if (_client) return _client.getFlag(name);
648
657
  return readGateOverride(name) ?? false;
649
658
  },
650
659
  getConfig(name, decode) {
660
+ const bs = getBootstrap();
661
+ if (bs !== null && name in bs.configs) {
662
+ const raw = bs.configs[name];
663
+ if (!decode) return raw;
664
+ try {
665
+ return decode(raw);
666
+ } catch {
667
+ return void 0;
668
+ }
669
+ }
651
670
  if (!_mountedAndReady) return void 0;
652
671
  if (_client) return _client.getConfig(name, decode);
653
672
  const ov = readConfigOverride(name);
@@ -675,10 +694,13 @@ var flags = {
675
694
  /**
676
695
  * Called by FlagsBoundary after React hydration to unlock flag reads.
677
696
  * Dispatches se:override:change so subscribers (FlagsBoundary) re-render
678
- * once with real values — URL overrides and server-evaluated flags.
697
+ * once with real values — URL overrides and CDN-loaded flags.
698
+ *
699
+ * Always dispatches even if already mounted: in React hydration-recovery
700
+ * renders the latch is already true so the early-return guard would swallow
701
+ * the event, leaving the re-mounted subtree stuck with stale (empty) values.
679
702
  */
680
703
  notifyMounted() {
681
- if (_mountedAndReady) return;
682
704
  _mountedAndReady = true;
683
705
  if (typeof window !== "undefined") {
684
706
  window.dispatchEvent(new CustomEvent("se:override:change"));
@@ -9,6 +9,11 @@ interface ExperimentResult<P> {
9
9
  group: string;
10
10
  params: P;
11
11
  }
12
+ interface BootstrapPayload {
13
+ flags: Record<string, boolean>;
14
+ configs: Record<string, unknown>;
15
+ experiments: Record<string, ExperimentResult<Record<string, unknown>>>;
16
+ }
12
17
  type FlagsClientEnv = "dev" | "staging" | "prod";
13
18
  interface FlagsClientOptions {
14
19
  apiKey: string;
@@ -39,6 +44,16 @@ declare class FlagsClient {
39
44
  getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
40
45
  getExperiment<P extends Record<string, unknown>>(name: string, user: User, defaultParams: P, decode?: (raw: unknown) => P): ExperimentResult<P>;
41
46
  track(userId: string, eventName: string, props?: Record<string, unknown>): void;
47
+ /**
48
+ * Evaluate all flags, configs, and experiments for a user against the locally
49
+ * cached blob (no network call). Applies ?se_ks_* / ?se_cf_* / ?se_exp_*
50
+ * overrides from the request URL when provided.
51
+ *
52
+ * Intended for SSR: call on the server, inject the result as
53
+ * `window.__SE_BOOTSTRAP` in the HTML, and the client SDK will read it
54
+ * synchronously without waiting for identify() to resolve.
55
+ */
56
+ evaluate(user: User, rawUrl?: string): BootstrapPayload;
42
57
  }
43
58
  interface LabelFile {
44
59
  v: number;
@@ -72,6 +87,12 @@ declare const flags: {
72
87
  getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
73
88
  getExperiment<P extends Record<string, unknown>>(name: string, user: User, defaultParams: P, decode?: (raw: unknown) => P): ExperimentResult<P>;
74
89
  track(userId: string, eventName: string, props?: Record<string, unknown>): void;
90
+ /**
91
+ * Evaluate all flags / configs / experiments for a user against the locally
92
+ * cached blob. Pass the request URL to apply ?se_ks_* / ?se_cf_* / ?se_exp_*
93
+ * overrides. Returns an empty payload when the blob hasn't been fetched yet.
94
+ */
95
+ evaluate(user: User, rawUrl?: string): BootstrapPayload;
75
96
  };
76
97
 
77
- export { type ExperimentResult, type FetchLabelsOptions, FlagsClient, type FlagsClientEnv, type FlagsClientOptions, type LabelFile, type User, _resetShipeasyServerForTests, configureShipeasyServer, fetchLabelsForSSR, flags, getShipeasyServerClient, version };
98
+ export { type BootstrapPayload, type ExperimentResult, type FetchLabelsOptions, FlagsClient, type FlagsClientEnv, type FlagsClientOptions, type LabelFile, type User, _resetShipeasyServerForTests, configureShipeasyServer, fetchLabelsForSSR, flags, getShipeasyServerClient, version };
@@ -9,6 +9,11 @@ interface ExperimentResult<P> {
9
9
  group: string;
10
10
  params: P;
11
11
  }
12
+ interface BootstrapPayload {
13
+ flags: Record<string, boolean>;
14
+ configs: Record<string, unknown>;
15
+ experiments: Record<string, ExperimentResult<Record<string, unknown>>>;
16
+ }
12
17
  type FlagsClientEnv = "dev" | "staging" | "prod";
13
18
  interface FlagsClientOptions {
14
19
  apiKey: string;
@@ -39,6 +44,16 @@ declare class FlagsClient {
39
44
  getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
40
45
  getExperiment<P extends Record<string, unknown>>(name: string, user: User, defaultParams: P, decode?: (raw: unknown) => P): ExperimentResult<P>;
41
46
  track(userId: string, eventName: string, props?: Record<string, unknown>): void;
47
+ /**
48
+ * Evaluate all flags, configs, and experiments for a user against the locally
49
+ * cached blob (no network call). Applies ?se_ks_* / ?se_cf_* / ?se_exp_*
50
+ * overrides from the request URL when provided.
51
+ *
52
+ * Intended for SSR: call on the server, inject the result as
53
+ * `window.__SE_BOOTSTRAP` in the HTML, and the client SDK will read it
54
+ * synchronously without waiting for identify() to resolve.
55
+ */
56
+ evaluate(user: User, rawUrl?: string): BootstrapPayload;
42
57
  }
43
58
  interface LabelFile {
44
59
  v: number;
@@ -72,6 +87,12 @@ declare const flags: {
72
87
  getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
73
88
  getExperiment<P extends Record<string, unknown>>(name: string, user: User, defaultParams: P, decode?: (raw: unknown) => P): ExperimentResult<P>;
74
89
  track(userId: string, eventName: string, props?: Record<string, unknown>): void;
90
+ /**
91
+ * Evaluate all flags / configs / experiments for a user against the locally
92
+ * cached blob. Pass the request URL to apply ?se_ks_* / ?se_cf_* / ?se_exp_*
93
+ * overrides. Returns an empty payload when the blob hasn't been fetched yet.
94
+ */
95
+ evaluate(user: User, rawUrl?: string): BootstrapPayload;
75
96
  };
76
97
 
77
- export { type ExperimentResult, type FetchLabelsOptions, FlagsClient, type FlagsClientEnv, type FlagsClientOptions, type LabelFile, type User, _resetShipeasyServerForTests, configureShipeasyServer, fetchLabelsForSSR, flags, getShipeasyServerClient, version };
98
+ export { type BootstrapPayload, type ExperimentResult, type FetchLabelsOptions, FlagsClient, type FlagsClientEnv, type FlagsClientOptions, type LabelFile, type User, _resetShipeasyServerForTests, configureShipeasyServer, fetchLabelsForSSR, flags, getShipeasyServerClient, version };
@@ -129,6 +129,51 @@ function evalGateInternal(gate, user) {
129
129
  if (!uid) return false;
130
130
  return murmur3(`${gate.salt}:${uid}`) % 1e4 < gate.rolloutPct;
131
131
  }
132
+ var TRUE_RX = /^(true|on|1|yes)$/i;
133
+ var FALSE_RX = /^(false|off|0|no)$/i;
134
+ function parseOverrideBool(raw) {
135
+ if (TRUE_RX.test(raw)) return true;
136
+ if (FALSE_RX.test(raw)) return false;
137
+ return null;
138
+ }
139
+ function decodeOverrideConfigValue(raw) {
140
+ if (raw.startsWith("b64:")) {
141
+ try {
142
+ const json = atob(raw.slice(4).replace(/-/g, "+").replace(/_/g, "/"));
143
+ return JSON.parse(json);
144
+ } catch {
145
+ return raw;
146
+ }
147
+ }
148
+ try {
149
+ return JSON.parse(raw);
150
+ } catch {
151
+ return raw;
152
+ }
153
+ }
154
+ function parseOverrides(rawUrl) {
155
+ const gates = {};
156
+ const configs = {};
157
+ const experiments = {};
158
+ try {
159
+ const url = new URL(rawUrl, "http://localhost");
160
+ for (const [k, v] of url.searchParams) {
161
+ if (k.startsWith("se_ks_")) {
162
+ const b = parseOverrideBool(v);
163
+ if (b !== null) gates[k.slice(6)] = b;
164
+ } else if (k.startsWith("se_cf_")) {
165
+ configs[k.slice(6)] = decodeOverrideConfigValue(v);
166
+ } else if (k.startsWith("se_config_")) {
167
+ configs[k.slice(10)] = decodeOverrideConfigValue(v);
168
+ } else if (k.startsWith("se_exp_")) {
169
+ const name = k.slice(7);
170
+ if (v && v !== "default" && v !== "none") experiments[name] = v;
171
+ }
172
+ }
173
+ } catch {
174
+ }
175
+ return { gates, configs, experiments };
176
+ }
132
177
  var FlagsClient = class {
133
178
  apiKey;
134
179
  baseUrl;
@@ -271,6 +316,38 @@ var FlagsClient = class {
271
316
  body
272
317
  }).catch((err) => console.warn("[shipeasy] track failed:", String(err)));
273
318
  }
319
+ /**
320
+ * Evaluate all flags, configs, and experiments for a user against the locally
321
+ * cached blob (no network call). Applies ?se_ks_* / ?se_cf_* / ?se_exp_*
322
+ * overrides from the request URL when provided.
323
+ *
324
+ * Intended for SSR: call on the server, inject the result as
325
+ * `window.__SE_BOOTSTRAP` in the HTML, and the client SDK will read it
326
+ * synchronously without waiting for identify() to resolve.
327
+ */
328
+ evaluate(user, rawUrl) {
329
+ const flags2 = {};
330
+ const configs = {};
331
+ const experiments = {};
332
+ for (const [name, gate] of Object.entries(this.flagsBlob?.gates ?? {})) {
333
+ flags2[name] = evalGateInternal(gate, user);
334
+ }
335
+ for (const [name, entry] of Object.entries(this.flagsBlob?.configs ?? {})) {
336
+ configs[name] = entry.value;
337
+ }
338
+ for (const [name] of Object.entries(this.expsBlob?.experiments ?? {})) {
339
+ experiments[name] = this.getExperiment(name, user, {});
340
+ }
341
+ if (rawUrl) {
342
+ const ov = parseOverrides(rawUrl);
343
+ Object.assign(flags2, ov.gates);
344
+ Object.assign(configs, ov.configs);
345
+ for (const [name, group] of Object.entries(ov.experiments)) {
346
+ experiments[name] = { inExperiment: true, group, params: {} };
347
+ }
348
+ }
349
+ return { flags: flags2, configs, experiments };
350
+ }
274
351
  };
275
352
  var DEFAULT_I18N_CDN = "https://cdn.i18n.shipeasy.ai";
276
353
  async function fetchJson(url, timeoutMs = 2e3) {
@@ -351,6 +428,14 @@ var flags = {
351
428
  },
352
429
  track(userId, eventName, props) {
353
430
  _server?.track(userId, eventName, props);
431
+ },
432
+ /**
433
+ * Evaluate all flags / configs / experiments for a user against the locally
434
+ * cached blob. Pass the request URL to apply ?se_ks_* / ?se_cf_* / ?se_exp_*
435
+ * overrides. Returns an empty payload when the blob hasn't been fetched yet.
436
+ */
437
+ evaluate(user, rawUrl) {
438
+ return _server?.evaluate(user, rawUrl) ?? { flags: {}, configs: {}, experiments: {} };
354
439
  }
355
440
  };
356
441
  // Annotate the CommonJS export names for ESM import in node:
@@ -99,6 +99,51 @@ function evalGateInternal(gate, user) {
99
99
  if (!uid) return false;
100
100
  return murmur3(`${gate.salt}:${uid}`) % 1e4 < gate.rolloutPct;
101
101
  }
102
+ var TRUE_RX = /^(true|on|1|yes)$/i;
103
+ var FALSE_RX = /^(false|off|0|no)$/i;
104
+ function parseOverrideBool(raw) {
105
+ if (TRUE_RX.test(raw)) return true;
106
+ if (FALSE_RX.test(raw)) return false;
107
+ return null;
108
+ }
109
+ function decodeOverrideConfigValue(raw) {
110
+ if (raw.startsWith("b64:")) {
111
+ try {
112
+ const json = atob(raw.slice(4).replace(/-/g, "+").replace(/_/g, "/"));
113
+ return JSON.parse(json);
114
+ } catch {
115
+ return raw;
116
+ }
117
+ }
118
+ try {
119
+ return JSON.parse(raw);
120
+ } catch {
121
+ return raw;
122
+ }
123
+ }
124
+ function parseOverrides(rawUrl) {
125
+ const gates = {};
126
+ const configs = {};
127
+ const experiments = {};
128
+ try {
129
+ const url = new URL(rawUrl, "http://localhost");
130
+ for (const [k, v] of url.searchParams) {
131
+ if (k.startsWith("se_ks_")) {
132
+ const b = parseOverrideBool(v);
133
+ if (b !== null) gates[k.slice(6)] = b;
134
+ } else if (k.startsWith("se_cf_")) {
135
+ configs[k.slice(6)] = decodeOverrideConfigValue(v);
136
+ } else if (k.startsWith("se_config_")) {
137
+ configs[k.slice(10)] = decodeOverrideConfigValue(v);
138
+ } else if (k.startsWith("se_exp_")) {
139
+ const name = k.slice(7);
140
+ if (v && v !== "default" && v !== "none") experiments[name] = v;
141
+ }
142
+ }
143
+ } catch {
144
+ }
145
+ return { gates, configs, experiments };
146
+ }
102
147
  var FlagsClient = class {
103
148
  apiKey;
104
149
  baseUrl;
@@ -241,6 +286,38 @@ var FlagsClient = class {
241
286
  body
242
287
  }).catch((err) => console.warn("[shipeasy] track failed:", String(err)));
243
288
  }
289
+ /**
290
+ * Evaluate all flags, configs, and experiments for a user against the locally
291
+ * cached blob (no network call). Applies ?se_ks_* / ?se_cf_* / ?se_exp_*
292
+ * overrides from the request URL when provided.
293
+ *
294
+ * Intended for SSR: call on the server, inject the result as
295
+ * `window.__SE_BOOTSTRAP` in the HTML, and the client SDK will read it
296
+ * synchronously without waiting for identify() to resolve.
297
+ */
298
+ evaluate(user, rawUrl) {
299
+ const flags2 = {};
300
+ const configs = {};
301
+ const experiments = {};
302
+ for (const [name, gate] of Object.entries(this.flagsBlob?.gates ?? {})) {
303
+ flags2[name] = evalGateInternal(gate, user);
304
+ }
305
+ for (const [name, entry] of Object.entries(this.flagsBlob?.configs ?? {})) {
306
+ configs[name] = entry.value;
307
+ }
308
+ for (const [name] of Object.entries(this.expsBlob?.experiments ?? {})) {
309
+ experiments[name] = this.getExperiment(name, user, {});
310
+ }
311
+ if (rawUrl) {
312
+ const ov = parseOverrides(rawUrl);
313
+ Object.assign(flags2, ov.gates);
314
+ Object.assign(configs, ov.configs);
315
+ for (const [name, group] of Object.entries(ov.experiments)) {
316
+ experiments[name] = { inExperiment: true, group, params: {} };
317
+ }
318
+ }
319
+ return { flags: flags2, configs, experiments };
320
+ }
244
321
  };
245
322
  var DEFAULT_I18N_CDN = "https://cdn.i18n.shipeasy.ai";
246
323
  async function fetchJson(url, timeoutMs = 2e3) {
@@ -321,6 +398,14 @@ var flags = {
321
398
  },
322
399
  track(userId, eventName, props) {
323
400
  _server?.track(userId, eventName, props);
401
+ },
402
+ /**
403
+ * Evaluate all flags / configs / experiments for a user against the locally
404
+ * cached blob. Pass the request URL to apply ?se_ks_* / ?se_cf_* / ?se_exp_*
405
+ * overrides. Returns an empty payload when the blob hasn't been fetched yet.
406
+ */
407
+ evaluate(user, rawUrl) {
408
+ return _server?.evaluate(user, rawUrl) ?? { flags: {}, configs: {}, experiments: {} };
324
409
  }
325
410
  };
326
411
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipeasy/sdk",
3
- "version": "2.0.1",
3
+ "version": "2.0.3",
4
4
  "description": "Shipeasy SDK — feature gates, runtime configs, experiments, and metrics for the Shipeasy hosted service.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "homepage": "https://shipeasy.ai",