@shipeasy/sdk 2.0.2 → 2.1.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.
@@ -110,6 +110,20 @@ interface AttachDevtoolsOptions {
110
110
  */
111
111
  declare function attachDevtools(client: FlagsClientBrowser, opts?: AttachDevtoolsOptions): () => void;
112
112
  /** Configure the singleton. Idempotent — re-calling with the same opts is a no-op. */
113
+ interface ShipeasyClientConfig {
114
+ /** SDK key — same value used on the server via shipeasy(). */
115
+ apiKey: string;
116
+ /** Override the ShipEasy CDN/edge base URL. Defaults to https://cdn.shipeasy.ai. */
117
+ baseUrl?: string;
118
+ /** Override the admin URL for the devtools overlay (dev use). */
119
+ adminUrl?: string;
120
+ }
121
+ /**
122
+ * Initialise the ShipEasy client SDK and wire up lazy devtools.
123
+ * Call this once at app startup (e.g. in a useEffect in your root layout).
124
+ * Returns a cleanup function — call it on unmount to remove event listeners.
125
+ */
126
+ declare function shipeasy(opts: ShipeasyClientConfig): () => void;
113
127
  declare function configureShipeasy(opts: FlagsClientBrowserOptions): FlagsClientBrowser;
114
128
  /** Returns the configured singleton, or null if configureShipeasy() hasn't run yet. */
115
129
  declare function getShipeasyClient(): FlagsClientBrowser | null;
@@ -126,6 +140,9 @@ interface BootstrapPayload {
126
140
  group: string;
127
141
  params: Record<string, unknown>;
128
142
  }>;
143
+ /** Set by getBootstrapHtml() for auto-init. Not part of evaluate() output. */
144
+ apiKey?: string;
145
+ apiUrl?: string;
129
146
  }
130
147
  /**
131
148
  * Universal flags facade. Methods return safe defaults when the singleton
@@ -137,10 +154,10 @@ declare const flags: {
137
154
  identify(user: User): Promise<void>;
138
155
  /**
139
156
  * Read a feature gate.
140
- * Priority: URL override → server bootstrap (window.__SE_BOOTSTRAP) → CDN-fetched (post-mount) → false.
141
- * The _mountedAndReady gate still applies for the CDN path to prevent hydration
142
- * mismatches on force-static pages; bootstrap data is safe to read immediately
143
- * because the server rendered with the same values.
157
+ * Priority: bootstrap → CDN/URL-override (post-mount) → false.
158
+ * Bootstrap is safe before mount because the server rendered with the same values.
159
+ * Everything else gates on _mountedAndReady to prevent hydration mismatches on
160
+ * force-static pages where SSR has no flag data.
144
161
  */
145
162
  get(name: string): boolean;
146
163
  getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
@@ -150,7 +167,11 @@ declare const flags: {
150
167
  /**
151
168
  * Called by FlagsBoundary after React hydration to unlock flag reads.
152
169
  * Dispatches se:override:change so subscribers (FlagsBoundary) re-render
153
- * once with real values — URL overrides and server-evaluated flags.
170
+ * once with real values — URL overrides and CDN-loaded flags.
171
+ *
172
+ * Always dispatches even if already mounted: in React hydration-recovery
173
+ * renders the latch is already true so the early-return guard would swallow
174
+ * the event, leaving the re-mounted subtree stuck with stale (empty) values.
154
175
  */
155
176
  notifyMounted(): void;
156
177
  /** Subscribe for change notifications (identify/override). Used by framework adapters. */
@@ -202,4 +223,4 @@ declare const i18n: {
202
223
  onUpdate(cb: () => void): () => void;
203
224
  };
204
225
 
205
- 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 };
226
+ 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 ShipeasyClientConfig, type ShipeasySdkBridge, type User, _resetShipeasyForTests, attachDevtools, configureShipeasy, encodeLabelMarker, flags, getShipeasyClient, i18n, isDevtoolsRequested, labelAttrs, loadDevtools, readConfigOverride, readExpOverride, readGateOverride, shipeasy, version };
@@ -110,6 +110,20 @@ interface AttachDevtoolsOptions {
110
110
  */
111
111
  declare function attachDevtools(client: FlagsClientBrowser, opts?: AttachDevtoolsOptions): () => void;
112
112
  /** Configure the singleton. Idempotent — re-calling with the same opts is a no-op. */
113
+ interface ShipeasyClientConfig {
114
+ /** SDK key — same value used on the server via shipeasy(). */
115
+ apiKey: string;
116
+ /** Override the ShipEasy CDN/edge base URL. Defaults to https://cdn.shipeasy.ai. */
117
+ baseUrl?: string;
118
+ /** Override the admin URL for the devtools overlay (dev use). */
119
+ adminUrl?: string;
120
+ }
121
+ /**
122
+ * Initialise the ShipEasy client SDK and wire up lazy devtools.
123
+ * Call this once at app startup (e.g. in a useEffect in your root layout).
124
+ * Returns a cleanup function — call it on unmount to remove event listeners.
125
+ */
126
+ declare function shipeasy(opts: ShipeasyClientConfig): () => void;
113
127
  declare function configureShipeasy(opts: FlagsClientBrowserOptions): FlagsClientBrowser;
114
128
  /** Returns the configured singleton, or null if configureShipeasy() hasn't run yet. */
115
129
  declare function getShipeasyClient(): FlagsClientBrowser | null;
@@ -126,6 +140,9 @@ interface BootstrapPayload {
126
140
  group: string;
127
141
  params: Record<string, unknown>;
128
142
  }>;
143
+ /** Set by getBootstrapHtml() for auto-init. Not part of evaluate() output. */
144
+ apiKey?: string;
145
+ apiUrl?: string;
129
146
  }
130
147
  /**
131
148
  * Universal flags facade. Methods return safe defaults when the singleton
@@ -137,10 +154,10 @@ declare const flags: {
137
154
  identify(user: User): Promise<void>;
138
155
  /**
139
156
  * Read a feature gate.
140
- * Priority: URL override → server bootstrap (window.__SE_BOOTSTRAP) → CDN-fetched (post-mount) → false.
141
- * The _mountedAndReady gate still applies for the CDN path to prevent hydration
142
- * mismatches on force-static pages; bootstrap data is safe to read immediately
143
- * because the server rendered with the same values.
157
+ * Priority: bootstrap → CDN/URL-override (post-mount) → false.
158
+ * Bootstrap is safe before mount because the server rendered with the same values.
159
+ * Everything else gates on _mountedAndReady to prevent hydration mismatches on
160
+ * force-static pages where SSR has no flag data.
144
161
  */
145
162
  get(name: string): boolean;
146
163
  getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
@@ -150,7 +167,11 @@ declare const flags: {
150
167
  /**
151
168
  * Called by FlagsBoundary after React hydration to unlock flag reads.
152
169
  * Dispatches se:override:change so subscribers (FlagsBoundary) re-render
153
- * once with real values — URL overrides and server-evaluated flags.
170
+ * once with real values — URL overrides and CDN-loaded flags.
171
+ *
172
+ * Always dispatches even if already mounted: in React hydration-recovery
173
+ * renders the latch is already true so the early-return guard would swallow
174
+ * the event, leaving the re-mounted subtree stuck with stale (empty) values.
154
175
  */
155
176
  notifyMounted(): void;
156
177
  /** Subscribe for change notifications (identify/override). Used by framework adapters. */
@@ -202,4 +223,4 @@ declare const i18n: {
202
223
  onUpdate(cb: () => void): () => void;
203
224
  };
204
225
 
205
- 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 };
226
+ 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 ShipeasyClientConfig, type ShipeasySdkBridge, type User, _resetShipeasyForTests, attachDevtools, configureShipeasy, encodeLabelMarker, flags, getShipeasyClient, i18n, isDevtoolsRequested, labelAttrs, loadDevtools, readConfigOverride, readExpOverride, readGateOverride, shipeasy, version };
@@ -38,6 +38,7 @@ __export(client_exports, {
38
38
  readConfigOverride: () => readConfigOverride,
39
39
  readExpOverride: () => readExpOverride,
40
40
  readGateOverride: () => readGateOverride,
41
+ shipeasy: () => shipeasy,
41
42
  version: () => version
42
43
  });
43
44
  module.exports = __toCommonJS(client_exports);
@@ -647,6 +648,14 @@ function attachDevtools(client, opts = {}) {
647
648
  };
648
649
  }
649
650
  var _client = null;
651
+ function shipeasy(opts) {
652
+ const client = configureShipeasy({
653
+ sdkKey: opts.apiKey,
654
+ baseUrl: opts.baseUrl ?? "https://cdn.shipeasy.ai"
655
+ });
656
+ flags.notifyMounted();
657
+ return attachDevtools(client, { adminUrl: opts.adminUrl });
658
+ }
650
659
  function configureShipeasy(opts) {
651
660
  if (_client) return _client;
652
661
  _client = new FlagsClientBrowser(opts);
@@ -686,30 +695,19 @@ var flags = {
686
695
  },
687
696
  /**
688
697
  * Read a feature gate.
689
- * Priority: URL override → server bootstrap (window.__SE_BOOTSTRAP) → CDN-fetched (post-mount) → false.
690
- * The _mountedAndReady gate still applies for the CDN path to prevent hydration
691
- * mismatches on force-static pages; bootstrap data is safe to read immediately
692
- * because the server rendered with the same values.
698
+ * Priority: bootstrap → CDN/URL-override (post-mount) → false.
699
+ * Bootstrap is safe before mount because the server rendered with the same values.
700
+ * Everything else gates on _mountedAndReady to prevent hydration mismatches on
701
+ * force-static pages where SSR has no flag data.
693
702
  */
694
703
  get(name) {
695
- const ov = readGateOverride(name);
696
- if (ov !== null) return ov;
697
704
  const bs = getBootstrap();
698
705
  if (bs !== null && name in bs.flags) return bs.flags[name];
699
706
  if (!_mountedAndReady) return false;
700
707
  if (_client) return _client.getFlag(name);
701
- return false;
708
+ return readGateOverride(name) ?? false;
702
709
  },
703
710
  getConfig(name, decode) {
704
- const ov = readConfigOverride(name);
705
- if (ov !== void 0) {
706
- if (!decode) return ov;
707
- try {
708
- return decode(ov);
709
- } catch {
710
- return void 0;
711
- }
712
- }
713
711
  const bs = getBootstrap();
714
712
  if (bs !== null && name in bs.configs) {
715
713
  const raw = bs.configs[name];
@@ -722,7 +720,14 @@ var flags = {
722
720
  }
723
721
  if (!_mountedAndReady) return void 0;
724
722
  if (_client) return _client.getConfig(name, decode);
725
- return void 0;
723
+ const ov = readConfigOverride(name);
724
+ if (ov === void 0) return void 0;
725
+ if (!decode) return ov;
726
+ try {
727
+ return decode(ov);
728
+ } catch {
729
+ return void 0;
730
+ }
726
731
  },
727
732
  getExperiment(name, defaultParams, decode, variants) {
728
733
  return _client?.getExperiment(name, defaultParams, decode, variants) ?? {
@@ -740,10 +745,13 @@ var flags = {
740
745
  /**
741
746
  * Called by FlagsBoundary after React hydration to unlock flag reads.
742
747
  * Dispatches se:override:change so subscribers (FlagsBoundary) re-render
743
- * once with real values — URL overrides and server-evaluated flags.
748
+ * once with real values — URL overrides and CDN-loaded flags.
749
+ *
750
+ * Always dispatches even if already mounted: in React hydration-recovery
751
+ * renders the latch is already true so the early-return guard would swallow
752
+ * the event, leaving the re-mounted subtree stuck with stale (empty) values.
744
753
  */
745
754
  notifyMounted() {
746
- if (_mountedAndReady) return;
747
755
  _mountedAndReady = true;
748
756
  if (typeof window !== "undefined") {
749
757
  window.dispatchEvent(new CustomEvent("se:override:change"));
@@ -775,9 +783,19 @@ function labelAttrs(key, variables, desc) {
775
783
  return attrs;
776
784
  }
777
785
  var _createElement = null;
786
+ var _I18N_SSR_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:ssr-i18n");
787
+ function getSSRI18nStore() {
788
+ return globalThis[_I18N_SSR_SYM]?.() ?? null;
789
+ }
790
+ function interpolate(raw, variables) {
791
+ if (!variables) return raw;
792
+ return raw.replace(/\{\{(\w+)\}\}/g, (_, k) => String(variables[k] ?? `{{${k}}}`));
793
+ }
778
794
  var i18n = {
779
795
  t(key, variables) {
780
796
  if (typeof window !== "undefined" && window.i18n) return window.i18n.t(key, variables);
797
+ const store = getSSRI18nStore();
798
+ if (store?.strings[key]) return interpolate(store.strings[key], variables);
781
799
  return key;
782
800
  },
783
801
  /**
@@ -793,7 +811,8 @@ var i18n = {
793
811
  * configured (e.g. server-side or in non-JSX contexts).
794
812
  */
795
813
  tEl(key, fallback, variables, desc) {
796
- const text = this.t(key, variables) || fallback;
814
+ const hasTranslation = typeof window !== "undefined" && Boolean(window.i18n) || Boolean(getSSRI18nStore()?.strings[key]);
815
+ const text = hasTranslation ? this.t(key, variables) || fallback : fallback;
797
816
  if (!_createElement) return text;
798
817
  return _createElement("span", labelAttrs(key, variables, desc), text);
799
818
  },
@@ -836,6 +855,12 @@ var i18n = {
836
855
  };
837
856
  }
838
857
  };
858
+ if (typeof window !== "undefined") {
859
+ const _initBs = window.__SE_BOOTSTRAP;
860
+ if (_initBs?.apiKey && !_client) {
861
+ shipeasy({ apiKey: _initBs.apiKey, baseUrl: _initBs.apiUrl });
862
+ }
863
+ }
839
864
  // Annotate the CommonJS export names for ESM import in node:
840
865
  0 && (module.exports = {
841
866
  FlagsClientBrowser,
@@ -856,5 +881,6 @@ var i18n = {
856
881
  readConfigOverride,
857
882
  readExpOverride,
858
883
  readGateOverride,
884
+ shipeasy,
859
885
  version
860
886
  });
@@ -605,6 +605,14 @@ function attachDevtools(client, opts = {}) {
605
605
  };
606
606
  }
607
607
  var _client = null;
608
+ function shipeasy(opts) {
609
+ const client = configureShipeasy({
610
+ sdkKey: opts.apiKey,
611
+ baseUrl: opts.baseUrl ?? "https://cdn.shipeasy.ai"
612
+ });
613
+ flags.notifyMounted();
614
+ return attachDevtools(client, { adminUrl: opts.adminUrl });
615
+ }
608
616
  function configureShipeasy(opts) {
609
617
  if (_client) return _client;
610
618
  _client = new FlagsClientBrowser(opts);
@@ -644,30 +652,19 @@ var flags = {
644
652
  },
645
653
  /**
646
654
  * Read a feature gate.
647
- * Priority: URL override → server bootstrap (window.__SE_BOOTSTRAP) → CDN-fetched (post-mount) → false.
648
- * The _mountedAndReady gate still applies for the CDN path to prevent hydration
649
- * mismatches on force-static pages; bootstrap data is safe to read immediately
650
- * because the server rendered with the same values.
655
+ * Priority: bootstrap → CDN/URL-override (post-mount) → false.
656
+ * Bootstrap is safe before mount because the server rendered with the same values.
657
+ * Everything else gates on _mountedAndReady to prevent hydration mismatches on
658
+ * force-static pages where SSR has no flag data.
651
659
  */
652
660
  get(name) {
653
- const ov = readGateOverride(name);
654
- if (ov !== null) return ov;
655
661
  const bs = getBootstrap();
656
662
  if (bs !== null && name in bs.flags) return bs.flags[name];
657
663
  if (!_mountedAndReady) return false;
658
664
  if (_client) return _client.getFlag(name);
659
- return false;
665
+ return readGateOverride(name) ?? false;
660
666
  },
661
667
  getConfig(name, decode) {
662
- const ov = readConfigOverride(name);
663
- if (ov !== void 0) {
664
- if (!decode) return ov;
665
- try {
666
- return decode(ov);
667
- } catch {
668
- return void 0;
669
- }
670
- }
671
668
  const bs = getBootstrap();
672
669
  if (bs !== null && name in bs.configs) {
673
670
  const raw = bs.configs[name];
@@ -680,7 +677,14 @@ var flags = {
680
677
  }
681
678
  if (!_mountedAndReady) return void 0;
682
679
  if (_client) return _client.getConfig(name, decode);
683
- return void 0;
680
+ const ov = readConfigOverride(name);
681
+ if (ov === void 0) return void 0;
682
+ if (!decode) return ov;
683
+ try {
684
+ return decode(ov);
685
+ } catch {
686
+ return void 0;
687
+ }
684
688
  },
685
689
  getExperiment(name, defaultParams, decode, variants) {
686
690
  return _client?.getExperiment(name, defaultParams, decode, variants) ?? {
@@ -698,10 +702,13 @@ var flags = {
698
702
  /**
699
703
  * Called by FlagsBoundary after React hydration to unlock flag reads.
700
704
  * Dispatches se:override:change so subscribers (FlagsBoundary) re-render
701
- * once with real values — URL overrides and server-evaluated flags.
705
+ * once with real values — URL overrides and CDN-loaded flags.
706
+ *
707
+ * Always dispatches even if already mounted: in React hydration-recovery
708
+ * renders the latch is already true so the early-return guard would swallow
709
+ * the event, leaving the re-mounted subtree stuck with stale (empty) values.
702
710
  */
703
711
  notifyMounted() {
704
- if (_mountedAndReady) return;
705
712
  _mountedAndReady = true;
706
713
  if (typeof window !== "undefined") {
707
714
  window.dispatchEvent(new CustomEvent("se:override:change"));
@@ -733,9 +740,19 @@ function labelAttrs(key, variables, desc) {
733
740
  return attrs;
734
741
  }
735
742
  var _createElement = null;
743
+ var _I18N_SSR_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:ssr-i18n");
744
+ function getSSRI18nStore() {
745
+ return globalThis[_I18N_SSR_SYM]?.() ?? null;
746
+ }
747
+ function interpolate(raw, variables) {
748
+ if (!variables) return raw;
749
+ return raw.replace(/\{\{(\w+)\}\}/g, (_, k) => String(variables[k] ?? `{{${k}}}`));
750
+ }
736
751
  var i18n = {
737
752
  t(key, variables) {
738
753
  if (typeof window !== "undefined" && window.i18n) return window.i18n.t(key, variables);
754
+ const store = getSSRI18nStore();
755
+ if (store?.strings[key]) return interpolate(store.strings[key], variables);
739
756
  return key;
740
757
  },
741
758
  /**
@@ -751,7 +768,8 @@ var i18n = {
751
768
  * configured (e.g. server-side or in non-JSX contexts).
752
769
  */
753
770
  tEl(key, fallback, variables, desc) {
754
- const text = this.t(key, variables) || fallback;
771
+ const hasTranslation = typeof window !== "undefined" && Boolean(window.i18n) || Boolean(getSSRI18nStore()?.strings[key]);
772
+ const text = hasTranslation ? this.t(key, variables) || fallback : fallback;
755
773
  if (!_createElement) return text;
756
774
  return _createElement("span", labelAttrs(key, variables, desc), text);
757
775
  },
@@ -794,6 +812,12 @@ var i18n = {
794
812
  };
795
813
  }
796
814
  };
815
+ if (typeof window !== "undefined") {
816
+ const _initBs = window.__SE_BOOTSTRAP;
817
+ if (_initBs?.apiKey && !_client) {
818
+ shipeasy({ apiKey: _initBs.apiKey, baseUrl: _initBs.apiUrl });
819
+ }
820
+ }
797
821
  export {
798
822
  FlagsClientBrowser,
799
823
  LABEL_MARKER_END,
@@ -813,5 +837,6 @@ export {
813
837
  readConfigOverride,
814
838
  readExpOverride,
815
839
  readGateOverride,
840
+ shipeasy,
816
841
  version
817
842
  };
@@ -55,6 +55,32 @@ declare class FlagsClient {
55
55
  */
56
56
  evaluate(user: User, rawUrl?: string): BootstrapPayload;
57
57
  }
58
+ interface I18nForRequest {
59
+ strings: Record<string, string>;
60
+ locale: string;
61
+ }
62
+ declare const i18n: {
63
+ /**
64
+ * Fetch translation labels for the current request and store them in an
65
+ * async-local context so `i18n.t()` / `i18n.tEl()` in SSR'd client
66
+ * components return the real translated strings instead of the key.
67
+ *
68
+ * Call once per request in the root layout (or page). Failure is silent —
69
+ * `i18n.t()` falls back to the hardcoded fallback arg when no labels are
70
+ * loaded.
71
+ *
72
+ * @param key SDK client key (NEXT_PUBLIC_SHIPEASY_CLIENT_KEY)
73
+ * @param profile i18n profile identifier, e.g. "en:prod"
74
+ * @param cdnBaseUrl Optional override for the i18n CDN (default: cdn.i18n.shipeasy.ai)
75
+ */
76
+ init(key: string, profile: string, cdnBaseUrl?: string): Promise<void>;
77
+ /**
78
+ * Return the translation strings loaded for the current request.
79
+ * Use this to include i18n data in the SSR bootstrap payload so the
80
+ * client doesn't need an extra network round-trip.
81
+ */
82
+ getForRequest(): I18nForRequest;
83
+ };
58
84
  interface LabelFile {
59
85
  v: number;
60
86
  profile: string;
@@ -72,6 +98,57 @@ declare function fetchLabelsForSSR(opts: FetchLabelsOptions): Promise<LabelFile
72
98
  declare function configureShipeasyServer(opts: FlagsClientOptions): FlagsClient;
73
99
  declare function getShipeasyServerClient(): FlagsClient | null;
74
100
  declare function _resetShipeasyServerForTests(): void;
101
+ interface ShipeasyServerConfig {
102
+ /**
103
+ * Server-side API key — authenticates flag/experiment fetches from the edge.
104
+ * Never embedded in browser output. A warning is logged if omitted.
105
+ */
106
+ apiKey?: string;
107
+ /**
108
+ * Public client key — embedded in window.__SE_BOOTSTRAP and used by the
109
+ * browser SDK. Safe to expose (e.g. NEXT_PUBLIC_ env vars).
110
+ * Defaults to apiKey for single-key setups.
111
+ */
112
+ clientKey?: string;
113
+ /** Raw URL or query string for applying ?se_ks_* / ?se_cf_* / ?se_exp_* overrides. */
114
+ urlOverrides?: string;
115
+ /** User attributes for flag and experiment evaluation. */
116
+ user?: User;
117
+ /** i18n profile to load for SSR translations, e.g. "en:prod". Defaults to "en:prod". */
118
+ i18nDefaultProfile?: string;
119
+ }
120
+ interface ShipeasyServerHandle {
121
+ flags: Record<string, boolean>;
122
+ configs: Record<string, unknown>;
123
+ experiments: Record<string, ExperimentResult<Record<string, unknown>>>;
124
+ /** Returns a vanilla-JS string for a single inline <script> tag. */
125
+ getBootstrapHtml(): string;
126
+ }
127
+ /**
128
+ * Initialise the ShipEasy server SDK, evaluate flags for this request, and
129
+ * return a handle. Call once per request in your root layout (or page for
130
+ * URL-override support). Failure is non-fatal — evaluation returns empty
131
+ * payloads and i18n falls back to hardcoded strings.
132
+ */
133
+ declare function shipeasy(opts: ShipeasyServerConfig): Promise<ShipeasyServerHandle>;
134
+ interface BootstrapHtmlOptions {
135
+ /** SDK client key */
136
+ apiKey: string;
137
+ /** i18n profile fed to the loader script. Defaults to "en:prod". */
138
+ i18nProfile?: string;
139
+ }
140
+ /**
141
+ * Returns a vanilla-JS script string for a single <script> tag.
142
+ * Handles everything the client needs at startup:
143
+ * - window.__se_devtools_config (when devtoolsAdminUrl is set)
144
+ * - window.__SE_BOOTSTRAP (flags + configs + experiments + i18n + apiKey for auto-init)
145
+ * - window.i18n shim from SSR strings (prevents hydration mismatches)
146
+ * - dynamic <script> injection for the i18n loader
147
+ *
148
+ * Framework-agnostic: set innerHTML on a <script> element, nothing else required.
149
+ * Pass null for bootstrap on pages without flag evaluation — client still auto-inits.
150
+ */
151
+ declare function getBootstrapHtml(bootstrap: BootstrapPayload | null, i18nData: I18nForRequest | null, opts: BootstrapHtmlOptions): string;
75
152
  declare const flags: {
76
153
  configure(opts: FlagsClientOptions): void;
77
154
  /**
@@ -95,4 +172,4 @@ declare const flags: {
95
172
  evaluate(user: User, rawUrl?: string): BootstrapPayload;
96
173
  };
97
174
 
98
- export { type BootstrapPayload, type ExperimentResult, type FetchLabelsOptions, FlagsClient, type FlagsClientEnv, type FlagsClientOptions, type LabelFile, type User, _resetShipeasyServerForTests, configureShipeasyServer, fetchLabelsForSSR, flags, getShipeasyServerClient, version };
175
+ export { type BootstrapHtmlOptions, type BootstrapPayload, type ExperimentResult, type FetchLabelsOptions, FlagsClient, type FlagsClientEnv, type FlagsClientOptions, type I18nForRequest, type LabelFile, type ShipeasyServerConfig, type ShipeasyServerHandle, type User, _resetShipeasyServerForTests, configureShipeasyServer, fetchLabelsForSSR, flags, getBootstrapHtml, getShipeasyServerClient, i18n, shipeasy, version };
@@ -55,6 +55,32 @@ declare class FlagsClient {
55
55
  */
56
56
  evaluate(user: User, rawUrl?: string): BootstrapPayload;
57
57
  }
58
+ interface I18nForRequest {
59
+ strings: Record<string, string>;
60
+ locale: string;
61
+ }
62
+ declare const i18n: {
63
+ /**
64
+ * Fetch translation labels for the current request and store them in an
65
+ * async-local context so `i18n.t()` / `i18n.tEl()` in SSR'd client
66
+ * components return the real translated strings instead of the key.
67
+ *
68
+ * Call once per request in the root layout (or page). Failure is silent —
69
+ * `i18n.t()` falls back to the hardcoded fallback arg when no labels are
70
+ * loaded.
71
+ *
72
+ * @param key SDK client key (NEXT_PUBLIC_SHIPEASY_CLIENT_KEY)
73
+ * @param profile i18n profile identifier, e.g. "en:prod"
74
+ * @param cdnBaseUrl Optional override for the i18n CDN (default: cdn.i18n.shipeasy.ai)
75
+ */
76
+ init(key: string, profile: string, cdnBaseUrl?: string): Promise<void>;
77
+ /**
78
+ * Return the translation strings loaded for the current request.
79
+ * Use this to include i18n data in the SSR bootstrap payload so the
80
+ * client doesn't need an extra network round-trip.
81
+ */
82
+ getForRequest(): I18nForRequest;
83
+ };
58
84
  interface LabelFile {
59
85
  v: number;
60
86
  profile: string;
@@ -72,6 +98,57 @@ declare function fetchLabelsForSSR(opts: FetchLabelsOptions): Promise<LabelFile
72
98
  declare function configureShipeasyServer(opts: FlagsClientOptions): FlagsClient;
73
99
  declare function getShipeasyServerClient(): FlagsClient | null;
74
100
  declare function _resetShipeasyServerForTests(): void;
101
+ interface ShipeasyServerConfig {
102
+ /**
103
+ * Server-side API key — authenticates flag/experiment fetches from the edge.
104
+ * Never embedded in browser output. A warning is logged if omitted.
105
+ */
106
+ apiKey?: string;
107
+ /**
108
+ * Public client key — embedded in window.__SE_BOOTSTRAP and used by the
109
+ * browser SDK. Safe to expose (e.g. NEXT_PUBLIC_ env vars).
110
+ * Defaults to apiKey for single-key setups.
111
+ */
112
+ clientKey?: string;
113
+ /** Raw URL or query string for applying ?se_ks_* / ?se_cf_* / ?se_exp_* overrides. */
114
+ urlOverrides?: string;
115
+ /** User attributes for flag and experiment evaluation. */
116
+ user?: User;
117
+ /** i18n profile to load for SSR translations, e.g. "en:prod". Defaults to "en:prod". */
118
+ i18nDefaultProfile?: string;
119
+ }
120
+ interface ShipeasyServerHandle {
121
+ flags: Record<string, boolean>;
122
+ configs: Record<string, unknown>;
123
+ experiments: Record<string, ExperimentResult<Record<string, unknown>>>;
124
+ /** Returns a vanilla-JS string for a single inline <script> tag. */
125
+ getBootstrapHtml(): string;
126
+ }
127
+ /**
128
+ * Initialise the ShipEasy server SDK, evaluate flags for this request, and
129
+ * return a handle. Call once per request in your root layout (or page for
130
+ * URL-override support). Failure is non-fatal — evaluation returns empty
131
+ * payloads and i18n falls back to hardcoded strings.
132
+ */
133
+ declare function shipeasy(opts: ShipeasyServerConfig): Promise<ShipeasyServerHandle>;
134
+ interface BootstrapHtmlOptions {
135
+ /** SDK client key */
136
+ apiKey: string;
137
+ /** i18n profile fed to the loader script. Defaults to "en:prod". */
138
+ i18nProfile?: string;
139
+ }
140
+ /**
141
+ * Returns a vanilla-JS script string for a single <script> tag.
142
+ * Handles everything the client needs at startup:
143
+ * - window.__se_devtools_config (when devtoolsAdminUrl is set)
144
+ * - window.__SE_BOOTSTRAP (flags + configs + experiments + i18n + apiKey for auto-init)
145
+ * - window.i18n shim from SSR strings (prevents hydration mismatches)
146
+ * - dynamic <script> injection for the i18n loader
147
+ *
148
+ * Framework-agnostic: set innerHTML on a <script> element, nothing else required.
149
+ * Pass null for bootstrap on pages without flag evaluation — client still auto-inits.
150
+ */
151
+ declare function getBootstrapHtml(bootstrap: BootstrapPayload | null, i18nData: I18nForRequest | null, opts: BootstrapHtmlOptions): string;
75
152
  declare const flags: {
76
153
  configure(opts: FlagsClientOptions): void;
77
154
  /**
@@ -95,4 +172,4 @@ declare const flags: {
95
172
  evaluate(user: User, rawUrl?: string): BootstrapPayload;
96
173
  };
97
174
 
98
- export { type BootstrapPayload, type ExperimentResult, type FetchLabelsOptions, FlagsClient, type FlagsClientEnv, type FlagsClientOptions, type LabelFile, type User, _resetShipeasyServerForTests, configureShipeasyServer, fetchLabelsForSSR, flags, getShipeasyServerClient, version };
175
+ export { type BootstrapHtmlOptions, type BootstrapPayload, type ExperimentResult, type FetchLabelsOptions, FlagsClient, type FlagsClientEnv, type FlagsClientOptions, type I18nForRequest, type LabelFile, type ShipeasyServerConfig, type ShipeasyServerHandle, type User, _resetShipeasyServerForTests, configureShipeasyServer, fetchLabelsForSSR, flags, getBootstrapHtml, getShipeasyServerClient, i18n, shipeasy, version };
@@ -25,7 +25,10 @@ __export(server_exports, {
25
25
  configureShipeasyServer: () => configureShipeasyServer,
26
26
  fetchLabelsForSSR: () => fetchLabelsForSSR,
27
27
  flags: () => flags,
28
+ getBootstrapHtml: () => getBootstrapHtml,
28
29
  getShipeasyServerClient: () => getShipeasyServerClient,
30
+ i18n: () => i18n,
31
+ shipeasy: () => shipeasy,
29
32
  version: () => version
30
33
  });
31
34
  module.exports = __toCommonJS(server_exports);
@@ -349,6 +352,39 @@ var FlagsClient = class {
349
352
  return { flags: flags2, configs, experiments };
350
353
  }
351
354
  };
355
+ var { AsyncLocalStorage } = require("async_hooks");
356
+ var _I18N_SSR_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:ssr-i18n");
357
+ var _i18nALS = new AsyncLocalStorage();
358
+ globalThis[_I18N_SSR_SYM] = () => _i18nALS.getStore() ?? null;
359
+ var i18n = {
360
+ /**
361
+ * Fetch translation labels for the current request and store them in an
362
+ * async-local context so `i18n.t()` / `i18n.tEl()` in SSR'd client
363
+ * components return the real translated strings instead of the key.
364
+ *
365
+ * Call once per request in the root layout (or page). Failure is silent —
366
+ * `i18n.t()` falls back to the hardcoded fallback arg when no labels are
367
+ * loaded.
368
+ *
369
+ * @param key SDK client key (NEXT_PUBLIC_SHIPEASY_CLIENT_KEY)
370
+ * @param profile i18n profile identifier, e.g. "en:prod"
371
+ * @param cdnBaseUrl Optional override for the i18n CDN (default: cdn.i18n.shipeasy.ai)
372
+ */
373
+ async init(key, profile, cdnBaseUrl) {
374
+ if (_i18nALS.getStore() !== void 0) return;
375
+ const labels = await fetchLabelsForSSR({ key, profile, cdnBaseUrl }).catch(() => null);
376
+ const locale = profile.split(":")[0] || "en";
377
+ _i18nALS.enterWith({ strings: labels?.strings ?? {}, locale });
378
+ },
379
+ /**
380
+ * Return the translation strings loaded for the current request.
381
+ * Use this to include i18n data in the SSR bootstrap payload so the
382
+ * client doesn't need an extra network round-trip.
383
+ */
384
+ getForRequest() {
385
+ return _i18nALS.getStore() ?? { strings: {}, locale: "en" };
386
+ }
387
+ };
352
388
  var DEFAULT_I18N_CDN = "https://cdn.i18n.shipeasy.ai";
353
389
  async function fetchJson(url, timeoutMs = 2e3) {
354
390
  const controller = new AbortController();
@@ -392,6 +428,51 @@ function _resetShipeasyServerForTests() {
392
428
  _server?.destroy();
393
429
  _server = null;
394
430
  }
431
+ async function shipeasy(opts) {
432
+ if (!opts.apiKey && !opts.clientKey) {
433
+ console.warn("[shipeasy] apiKey is required \u2014 flag evaluation and i18n will not load.");
434
+ } else if (!opts.apiKey) {
435
+ console.warn("[shipeasy] apiKey not set \u2014 falling back to clientKey for server requests.");
436
+ }
437
+ const apiKey = opts.apiKey ?? opts.clientKey ?? "";
438
+ const clientKey = opts.clientKey ?? opts.apiKey ?? "";
439
+ const profile = opts.i18nDefaultProfile ?? "en:prod";
440
+ flags.configure({ apiKey });
441
+ await Promise.allSettled([flags.initOnce(), i18n.init(apiKey, profile)]);
442
+ const bootstrap = flags.evaluate(opts.user ?? {}, opts.urlOverrides);
443
+ const i18nData = i18n.getForRequest();
444
+ return {
445
+ flags: bootstrap.flags,
446
+ configs: bootstrap.configs,
447
+ experiments: bootstrap.experiments,
448
+ getBootstrapHtml() {
449
+ return getBootstrapHtml(bootstrap, i18nData, { apiKey: clientKey });
450
+ }
451
+ };
452
+ }
453
+ function getBootstrapHtml(bootstrap, i18nData, opts) {
454
+ const parts = [];
455
+ const apiUrl = "https://cdn.shipeasy.ai";
456
+ const profile = opts.i18nProfile ?? "en:prod";
457
+ const payload = {
458
+ flags: bootstrap?.flags ?? {},
459
+ configs: bootstrap?.configs ?? {},
460
+ experiments: bootstrap?.experiments ?? {},
461
+ apiKey: opts.apiKey,
462
+ apiUrl
463
+ };
464
+ if (i18nData) payload.i18n = i18nData;
465
+ parts.push(`window.__SE_BOOTSTRAP=${JSON.stringify(payload)};`);
466
+ if (i18nData?.strings && Object.keys(i18nData.strings).length > 0) {
467
+ parts.push(
468
+ `(function(){var d=window.__SE_BOOTSTRAP.i18n;if(!d)return;window.i18n={locale:d.locale,t:function(k,v){var r=d.strings[k];if(!r)return k;return v?r.replace(/\\{\\{(\\w+)\\}\\}/g,function(_,p){return v[p]!==undefined?String(v[p]):'{{'+p+'}}'}):r;},on:function(){return function(){};}};})();`
469
+ );
470
+ }
471
+ parts.push(
472
+ `(function(){var s=document.createElement('script');s.src=${JSON.stringify(`${apiUrl}/sdk/i18n/loader.js`)};s.setAttribute('data-key',${JSON.stringify(opts.apiKey)});s.setAttribute('data-profile',${JSON.stringify(profile)});document.head.appendChild(s);})();`
473
+ );
474
+ return parts.join("");
475
+ }
395
476
  var flags = {
396
477
  configure(opts) {
397
478
  configureShipeasyServer(opts);
@@ -445,6 +526,9 @@ var flags = {
445
526
  configureShipeasyServer,
446
527
  fetchLabelsForSSR,
447
528
  flags,
529
+ getBootstrapHtml,
448
530
  getShipeasyServerClient,
531
+ i18n,
532
+ shipeasy,
449
533
  version
450
534
  });
@@ -1,3 +1,10 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined") return require.apply(this, arguments);
5
+ throw Error('Dynamic require of "' + x + '" is not supported');
6
+ });
7
+
1
8
  // src/server/index.ts
2
9
  var version = "1.0.0";
3
10
  var C1 = 3432918353;
@@ -319,6 +326,39 @@ var FlagsClient = class {
319
326
  return { flags: flags2, configs, experiments };
320
327
  }
321
328
  };
329
+ var { AsyncLocalStorage } = __require("async_hooks");
330
+ var _I18N_SSR_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:ssr-i18n");
331
+ var _i18nALS = new AsyncLocalStorage();
332
+ globalThis[_I18N_SSR_SYM] = () => _i18nALS.getStore() ?? null;
333
+ var i18n = {
334
+ /**
335
+ * Fetch translation labels for the current request and store them in an
336
+ * async-local context so `i18n.t()` / `i18n.tEl()` in SSR'd client
337
+ * components return the real translated strings instead of the key.
338
+ *
339
+ * Call once per request in the root layout (or page). Failure is silent —
340
+ * `i18n.t()` falls back to the hardcoded fallback arg when no labels are
341
+ * loaded.
342
+ *
343
+ * @param key SDK client key (NEXT_PUBLIC_SHIPEASY_CLIENT_KEY)
344
+ * @param profile i18n profile identifier, e.g. "en:prod"
345
+ * @param cdnBaseUrl Optional override for the i18n CDN (default: cdn.i18n.shipeasy.ai)
346
+ */
347
+ async init(key, profile, cdnBaseUrl) {
348
+ if (_i18nALS.getStore() !== void 0) return;
349
+ const labels = await fetchLabelsForSSR({ key, profile, cdnBaseUrl }).catch(() => null);
350
+ const locale = profile.split(":")[0] || "en";
351
+ _i18nALS.enterWith({ strings: labels?.strings ?? {}, locale });
352
+ },
353
+ /**
354
+ * Return the translation strings loaded for the current request.
355
+ * Use this to include i18n data in the SSR bootstrap payload so the
356
+ * client doesn't need an extra network round-trip.
357
+ */
358
+ getForRequest() {
359
+ return _i18nALS.getStore() ?? { strings: {}, locale: "en" };
360
+ }
361
+ };
322
362
  var DEFAULT_I18N_CDN = "https://cdn.i18n.shipeasy.ai";
323
363
  async function fetchJson(url, timeoutMs = 2e3) {
324
364
  const controller = new AbortController();
@@ -362,6 +402,51 @@ function _resetShipeasyServerForTests() {
362
402
  _server?.destroy();
363
403
  _server = null;
364
404
  }
405
+ async function shipeasy(opts) {
406
+ if (!opts.apiKey && !opts.clientKey) {
407
+ console.warn("[shipeasy] apiKey is required \u2014 flag evaluation and i18n will not load.");
408
+ } else if (!opts.apiKey) {
409
+ console.warn("[shipeasy] apiKey not set \u2014 falling back to clientKey for server requests.");
410
+ }
411
+ const apiKey = opts.apiKey ?? opts.clientKey ?? "";
412
+ const clientKey = opts.clientKey ?? opts.apiKey ?? "";
413
+ const profile = opts.i18nDefaultProfile ?? "en:prod";
414
+ flags.configure({ apiKey });
415
+ await Promise.allSettled([flags.initOnce(), i18n.init(apiKey, profile)]);
416
+ const bootstrap = flags.evaluate(opts.user ?? {}, opts.urlOverrides);
417
+ const i18nData = i18n.getForRequest();
418
+ return {
419
+ flags: bootstrap.flags,
420
+ configs: bootstrap.configs,
421
+ experiments: bootstrap.experiments,
422
+ getBootstrapHtml() {
423
+ return getBootstrapHtml(bootstrap, i18nData, { apiKey: clientKey });
424
+ }
425
+ };
426
+ }
427
+ function getBootstrapHtml(bootstrap, i18nData, opts) {
428
+ const parts = [];
429
+ const apiUrl = "https://cdn.shipeasy.ai";
430
+ const profile = opts.i18nProfile ?? "en:prod";
431
+ const payload = {
432
+ flags: bootstrap?.flags ?? {},
433
+ configs: bootstrap?.configs ?? {},
434
+ experiments: bootstrap?.experiments ?? {},
435
+ apiKey: opts.apiKey,
436
+ apiUrl
437
+ };
438
+ if (i18nData) payload.i18n = i18nData;
439
+ parts.push(`window.__SE_BOOTSTRAP=${JSON.stringify(payload)};`);
440
+ if (i18nData?.strings && Object.keys(i18nData.strings).length > 0) {
441
+ parts.push(
442
+ `(function(){var d=window.__SE_BOOTSTRAP.i18n;if(!d)return;window.i18n={locale:d.locale,t:function(k,v){var r=d.strings[k];if(!r)return k;return v?r.replace(/\\{\\{(\\w+)\\}\\}/g,function(_,p){return v[p]!==undefined?String(v[p]):'{{'+p+'}}'}):r;},on:function(){return function(){};}};})();`
443
+ );
444
+ }
445
+ parts.push(
446
+ `(function(){var s=document.createElement('script');s.src=${JSON.stringify(`${apiUrl}/sdk/i18n/loader.js`)};s.setAttribute('data-key',${JSON.stringify(opts.apiKey)});s.setAttribute('data-profile',${JSON.stringify(profile)});document.head.appendChild(s);})();`
447
+ );
448
+ return parts.join("");
449
+ }
365
450
  var flags = {
366
451
  configure(opts) {
367
452
  configureShipeasyServer(opts);
@@ -414,6 +499,9 @@ export {
414
499
  configureShipeasyServer,
415
500
  fetchLabelsForSSR,
416
501
  flags,
502
+ getBootstrapHtml,
417
503
  getShipeasyServerClient,
504
+ i18n,
505
+ shipeasy,
418
506
  version
419
507
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipeasy/sdk",
3
- "version": "2.0.2",
3
+ "version": "2.1.0",
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",
@@ -48,13 +48,6 @@
48
48
  "LICENSE",
49
49
  "README.md"
50
50
  ],
51
- "scripts": {
52
- "build": "tsup",
53
- "type-check": "tsc --noEmit",
54
- "test": "vitest run",
55
- "test:watch": "vitest",
56
- "publish-loader": "wrangler r2 object put shipeasy-sdk/loader-v$npm_package_version.js --file=dist/loader/loader.global.js --content-type 'application/javascript; charset=utf-8' --cache-control 'public, max-age=31536000, immutable' --remote && wrangler r2 object put shipeasy-sdk/loader.js --file=dist/loader/loader.global.js --content-type 'application/javascript; charset=utf-8' --cache-control 'public, max-age=300' --remote"
57
- },
58
51
  "dependencies": {
59
52
  "murmurhash-js": "^1.0.0"
60
53
  },
@@ -78,5 +71,12 @@
78
71
  },
79
72
  "engines": {
80
73
  "node": ">=20"
74
+ },
75
+ "scripts": {
76
+ "build": "tsup",
77
+ "type-check": "tsc --noEmit",
78
+ "test": "vitest run",
79
+ "test:watch": "vitest",
80
+ "publish-loader": "wrangler r2 object put shipeasy-sdk/loader-v$npm_package_version.js --file=dist/loader/loader.global.js --content-type 'application/javascript; charset=utf-8' --cache-control 'public, max-age=31536000, immutable' --remote && wrangler r2 object put shipeasy-sdk/loader.js --file=dist/loader/loader.global.js --content-type 'application/javascript; charset=utf-8' --cache-control 'public, max-age=300' --remote"
81
81
  }
82
- }
82
+ }