@shipeasy/sdk 2.5.2 → 3.0.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.
@@ -137,12 +137,23 @@ interface AttachDevtoolsOptions {
137
137
  declare function attachDevtools(client: FlagsClientBrowser, opts?: AttachDevtoolsOptions): () => void;
138
138
  /** Configure the singleton. Idempotent — re-calling with the same opts is a no-op. */
139
139
  interface ShipeasyClientConfig {
140
- /** SDK key — same value used on the server via shipeasy(). */
141
- apiKey: string;
140
+ /**
141
+ * Public client key — the ONLY key the browser entrypoint accepts. Authenticates
142
+ * /sdk/evaluate, /collect and the runtime i18n loader (/sdk/i18n/strings). Safe to
143
+ * expose (e.g. NEXT_PUBLIC_ env vars). This is a different key from the server key
144
+ * passed to `shipeasy({ serverKey })` in @shipeasy/sdk/server — never use the
145
+ * server key here.
146
+ */
147
+ clientKey: string;
142
148
  /** Override the ShipEasy CDN/edge base URL. Defaults to https://cdn.shipeasy.ai. */
143
149
  baseUrl?: string;
144
150
  /** Override the admin URL for the devtools overlay (dev use). */
145
151
  adminUrl?: string;
152
+ /**
153
+ * i18n profile for the runtime string loader, e.g. "en:prod". Defaults to the
154
+ * profile the server recorded in window.__SE_BOOTSTRAP, then "en:prod".
155
+ */
156
+ i18nProfile?: string;
146
157
  /**
147
158
  * Skip the lazy auto-identify({}) at boot. Defaults to true (auto-identify on).
148
159
  * Turn off when the host has its own identify orchestration and wants to
@@ -158,9 +169,9 @@ interface ShipeasyClientConfig {
158
169
  * Pass `false` to disable everything, or a per-group object to narrow:
159
170
  *
160
171
  * ```ts
161
- * shipeasy({ apiKey, autoCollect: false }); // off
162
- * shipeasy({ apiKey, autoCollect: { errors: false } }); // vitals + engagement only
163
- * shipeasy({ apiKey }); // all groups on
172
+ * shipeasy({ clientKey, autoCollect: false }); // off
173
+ * shipeasy({ clientKey, autoCollect: { errors: false } }); // vitals + engagement only
174
+ * shipeasy({ clientKey }); // all groups on
164
175
  * ```
165
176
  */
166
177
  autoCollect?: boolean | Partial<AutoCollectGroups>;
@@ -198,8 +209,8 @@ interface BootstrapPayload {
198
209
  * the killswitch is not whole-killed and the map carries per-switch state.
199
210
  */
200
211
  killswitches?: Record<string, boolean | Record<string, boolean>>;
201
- /** Set by getBootstrapHtml() for auto-init. Not part of evaluate() output. */
202
- apiKey?: string;
212
+ /** i18n profile the server rendered with, so the client loader matches. No key is embedded. */
213
+ i18nProfile?: string;
203
214
  apiUrl?: string;
204
215
  /** When true, tEl() returns marker-wrapped strings for devtools label editing. */
205
216
  editLabels?: boolean;
@@ -137,12 +137,23 @@ interface AttachDevtoolsOptions {
137
137
  declare function attachDevtools(client: FlagsClientBrowser, opts?: AttachDevtoolsOptions): () => void;
138
138
  /** Configure the singleton. Idempotent — re-calling with the same opts is a no-op. */
139
139
  interface ShipeasyClientConfig {
140
- /** SDK key — same value used on the server via shipeasy(). */
141
- apiKey: string;
140
+ /**
141
+ * Public client key — the ONLY key the browser entrypoint accepts. Authenticates
142
+ * /sdk/evaluate, /collect and the runtime i18n loader (/sdk/i18n/strings). Safe to
143
+ * expose (e.g. NEXT_PUBLIC_ env vars). This is a different key from the server key
144
+ * passed to `shipeasy({ serverKey })` in @shipeasy/sdk/server — never use the
145
+ * server key here.
146
+ */
147
+ clientKey: string;
142
148
  /** Override the ShipEasy CDN/edge base URL. Defaults to https://cdn.shipeasy.ai. */
143
149
  baseUrl?: string;
144
150
  /** Override the admin URL for the devtools overlay (dev use). */
145
151
  adminUrl?: string;
152
+ /**
153
+ * i18n profile for the runtime string loader, e.g. "en:prod". Defaults to the
154
+ * profile the server recorded in window.__SE_BOOTSTRAP, then "en:prod".
155
+ */
156
+ i18nProfile?: string;
146
157
  /**
147
158
  * Skip the lazy auto-identify({}) at boot. Defaults to true (auto-identify on).
148
159
  * Turn off when the host has its own identify orchestration and wants to
@@ -158,9 +169,9 @@ interface ShipeasyClientConfig {
158
169
  * Pass `false` to disable everything, or a per-group object to narrow:
159
170
  *
160
171
  * ```ts
161
- * shipeasy({ apiKey, autoCollect: false }); // off
162
- * shipeasy({ apiKey, autoCollect: { errors: false } }); // vitals + engagement only
163
- * shipeasy({ apiKey }); // all groups on
172
+ * shipeasy({ clientKey, autoCollect: false }); // off
173
+ * shipeasy({ clientKey, autoCollect: { errors: false } }); // vitals + engagement only
174
+ * shipeasy({ clientKey }); // all groups on
164
175
  * ```
165
176
  */
166
177
  autoCollect?: boolean | Partial<AutoCollectGroups>;
@@ -198,8 +209,8 @@ interface BootstrapPayload {
198
209
  * the killswitch is not whole-killed and the map carries per-switch state.
199
210
  */
200
211
  killswitches?: Record<string, boolean | Record<string, boolean>>;
201
- /** Set by getBootstrapHtml() for auto-init. Not part of evaluate() output. */
202
- apiKey?: string;
212
+ /** i18n profile the server rendered with, so the client loader matches. No key is embedded. */
213
+ i18nProfile?: string;
203
214
  apiUrl?: string;
204
215
  /** When true, tEl() returns marker-wrapped strings for devtools label editing. */
205
216
  editLabels?: boolean;
@@ -711,12 +711,14 @@ function shipeasy(opts) {
711
711
  const ac = opts.autoCollect;
712
712
  const blanket = ac === false ? false : true;
713
713
  const groups = ac && typeof ac === "object" ? ac : void 0;
714
+ const baseUrl = opts.baseUrl ?? "https://cdn.shipeasy.ai";
714
715
  const client = configureShipeasy({
715
- sdkKey: opts.apiKey,
716
- baseUrl: opts.baseUrl ?? "https://cdn.shipeasy.ai",
716
+ sdkKey: opts.clientKey,
717
+ baseUrl,
717
718
  autoGuardrails: blanket,
718
719
  autoGuardrailGroups: groups
719
720
  });
721
+ injectI18nLoader(opts.clientKey, baseUrl, opts.i18nProfile);
720
722
  flags.notifyMounted();
721
723
  if (opts.autoIdentify !== false) {
722
724
  void client.identify({}).catch((err) => {
@@ -736,6 +738,23 @@ function getShipeasyClient() {
736
738
  function _resetShipeasyForTests() {
737
739
  _client?.destroy();
738
740
  _client = null;
741
+ _i18nLoaderInjected = false;
742
+ }
743
+ var _i18nLoaderInjected = false;
744
+ function injectI18nLoader(clientKey, baseUrl, profileOpt) {
745
+ if (_i18nLoaderInjected || typeof document === "undefined") return;
746
+ if (!clientKey || typeof document.createElement !== "function" || !document.head) return;
747
+ _i18nLoaderInjected = true;
748
+ try {
749
+ const bs = getBootstrap();
750
+ const profile = profileOpt ?? bs?.i18nProfile ?? "en:prod";
751
+ const s = document.createElement("script");
752
+ s.src = `${baseUrl}/sdk/i18n/loader.js`;
753
+ s.setAttribute("data-key", clientKey);
754
+ s.setAttribute("data-profile", profile);
755
+ document.head.appendChild(s);
756
+ } catch {
757
+ }
739
758
  }
740
759
  function getBootstrap() {
741
760
  if (typeof window === "undefined") return null;
@@ -1090,12 +1109,6 @@ var i18n = {
1090
1109
  };
1091
1110
  }
1092
1111
  };
1093
- if (typeof window !== "undefined") {
1094
- const _initBs = window.__SE_BOOTSTRAP;
1095
- if (_initBs?.apiKey && !_client) {
1096
- shipeasy({ apiKey: _initBs.apiKey, baseUrl: _initBs.apiUrl });
1097
- }
1098
- }
1099
1112
  // Annotate the CommonJS export names for ESM import in node:
1100
1113
  0 && (module.exports = {
1101
1114
  FlagsClientBrowser,
@@ -668,12 +668,14 @@ function shipeasy(opts) {
668
668
  const ac = opts.autoCollect;
669
669
  const blanket = ac === false ? false : true;
670
670
  const groups = ac && typeof ac === "object" ? ac : void 0;
671
+ const baseUrl = opts.baseUrl ?? "https://cdn.shipeasy.ai";
671
672
  const client = configureShipeasy({
672
- sdkKey: opts.apiKey,
673
- baseUrl: opts.baseUrl ?? "https://cdn.shipeasy.ai",
673
+ sdkKey: opts.clientKey,
674
+ baseUrl,
674
675
  autoGuardrails: blanket,
675
676
  autoGuardrailGroups: groups
676
677
  });
678
+ injectI18nLoader(opts.clientKey, baseUrl, opts.i18nProfile);
677
679
  flags.notifyMounted();
678
680
  if (opts.autoIdentify !== false) {
679
681
  void client.identify({}).catch((err) => {
@@ -693,6 +695,23 @@ function getShipeasyClient() {
693
695
  function _resetShipeasyForTests() {
694
696
  _client?.destroy();
695
697
  _client = null;
698
+ _i18nLoaderInjected = false;
699
+ }
700
+ var _i18nLoaderInjected = false;
701
+ function injectI18nLoader(clientKey, baseUrl, profileOpt) {
702
+ if (_i18nLoaderInjected || typeof document === "undefined") return;
703
+ if (!clientKey || typeof document.createElement !== "function" || !document.head) return;
704
+ _i18nLoaderInjected = true;
705
+ try {
706
+ const bs = getBootstrap();
707
+ const profile = profileOpt ?? bs?.i18nProfile ?? "en:prod";
708
+ const s = document.createElement("script");
709
+ s.src = `${baseUrl}/sdk/i18n/loader.js`;
710
+ s.setAttribute("data-key", clientKey);
711
+ s.setAttribute("data-profile", profile);
712
+ document.head.appendChild(s);
713
+ } catch {
714
+ }
696
715
  }
697
716
  function getBootstrap() {
698
717
  if (typeof window === "undefined") return null;
@@ -1047,12 +1066,6 @@ var i18n = {
1047
1066
  };
1048
1067
  }
1049
1068
  };
1050
- if (typeof window !== "undefined") {
1051
- const _initBs = window.__SE_BOOTSTRAP;
1052
- if (_initBs?.apiKey && !_client) {
1053
- shipeasy({ apiKey: _initBs.apiKey, baseUrl: _initBs.apiUrl });
1054
- }
1055
- }
1056
1069
  export {
1057
1070
  FlagsClientBrowser,
1058
1071
  LABEL_MARKER_END,
@@ -132,20 +132,15 @@ declare function getShipeasyServerClient(): FlagsClient | null;
132
132
  declare function _resetShipeasyServerForTests(): void;
133
133
  interface ShipeasyServerConfig {
134
134
  /**
135
- * Server-side API key — authenticates flag/experiment fetches from the edge
136
- * (requireKey("server")). Never embedded in browser output. If omitted, flag
137
- * and experiment evaluation is skipped and an error is logged — it is NOT
138
- * substituted with clientKey (a client key 401s against /sdk/flags).
135
+ * Server key — the ONLY key the server entrypoint accepts. Authenticates
136
+ * flag/experiment fetches (requireKey("server")) AND SSR i18n string fetches
137
+ * (the /sdk/i18n/strings route accepts the server key for server-side use).
138
+ * Never embedded in browser output. The browser uses its own client key via
139
+ * `shipeasy({ clientKey })` from `@shipeasy/sdk/client` — the server never
140
+ * sees or forwards the client key. If omitted, flag/experiment/i18n loading
141
+ * is skipped and an error is logged.
139
142
  */
140
- apiKey?: string;
141
- /**
142
- * Public client key — embedded in window.__SE_BOOTSTRAP and used by the
143
- * browser SDK, and authenticates i18n string fetches (requireKey("client")).
144
- * Safe to expose (e.g. NEXT_PUBLIC_ env vars). If omitted, i18n loading is
145
- * skipped and an error is logged — it is NOT substituted with apiKey (a
146
- * server key 401s against /sdk/i18n/strings).
147
- */
148
- clientKey?: string;
143
+ serverKey?: string;
149
144
  /** Raw URL or query string for applying ?se_ks_* / ?se_cf_* / ?se_exp_* overrides. */
150
145
  urlOverrides?: string;
151
146
  /** User attributes for flag and experiment evaluation. */
@@ -168,23 +163,24 @@ interface ShipeasyServerHandle {
168
163
  */
169
164
  declare function shipeasy(opts: ShipeasyServerConfig): Promise<ShipeasyServerHandle>;
170
165
  interface BootstrapHtmlOptions {
171
- /** SDK client key */
172
- apiKey: string;
173
- /** i18n profile fed to the loader script. Defaults to "en:prod". */
166
+ /** i18n profile recorded in the bootstrap so the client loader matches SSR. Defaults to "en:prod". */
174
167
  i18nProfile?: string;
175
168
  /** When true, tEl() embeds label markers so the devtools can highlight them. */
176
169
  editLabels?: boolean;
177
170
  }
178
171
  /**
179
- * Returns a vanilla-JS script string for a single <script> tag.
180
- * Handles everything the client needs at startup:
172
+ * Returns a vanilla-JS string for a single inline <script> tag. Handles
173
+ * everything the client needs at startup EXCEPT the key — no SDK key is ever
174
+ * embedded here (the server only knows the server key, which must stay
175
+ * server-side). The browser supplies its own client key via
176
+ * `shipeasy({ clientKey })` from @shipeasy/sdk/client, which also injects the
177
+ * runtime i18n loader. This script emits:
181
178
  * - window.__se_devtools_config (when devtoolsAdminUrl is set)
182
- * - window.__SE_BOOTSTRAP (flags + configs + experiments + i18n + apiKey for auto-init)
183
- * - window.i18n shim from SSR strings (prevents hydration mismatches)
184
- * - dynamic <script> injection for the i18n loader
179
+ * - window.__SE_BOOTSTRAP (flags + configs + experiments + i18n DATA + i18nProfile, NO key)
180
+ * - window.i18n shim from SSR strings (prevents hydration mismatches / FOUC)
181
+ * - devtools overlay loader when ?se / ?se_devtools is present
185
182
  *
186
183
  * Framework-agnostic: set innerHTML on a <script> element, nothing else required.
187
- * Pass null for bootstrap on pages without flag evaluation — client still auto-inits.
188
184
  */
189
185
  declare function getBootstrapHtml(bootstrap: BootstrapPayload | null, i18nData: I18nForRequest | null, opts: BootstrapHtmlOptions): string;
190
186
  declare const flags: {
@@ -132,20 +132,15 @@ declare function getShipeasyServerClient(): FlagsClient | null;
132
132
  declare function _resetShipeasyServerForTests(): void;
133
133
  interface ShipeasyServerConfig {
134
134
  /**
135
- * Server-side API key — authenticates flag/experiment fetches from the edge
136
- * (requireKey("server")). Never embedded in browser output. If omitted, flag
137
- * and experiment evaluation is skipped and an error is logged — it is NOT
138
- * substituted with clientKey (a client key 401s against /sdk/flags).
135
+ * Server key — the ONLY key the server entrypoint accepts. Authenticates
136
+ * flag/experiment fetches (requireKey("server")) AND SSR i18n string fetches
137
+ * (the /sdk/i18n/strings route accepts the server key for server-side use).
138
+ * Never embedded in browser output. The browser uses its own client key via
139
+ * `shipeasy({ clientKey })` from `@shipeasy/sdk/client` — the server never
140
+ * sees or forwards the client key. If omitted, flag/experiment/i18n loading
141
+ * is skipped and an error is logged.
139
142
  */
140
- apiKey?: string;
141
- /**
142
- * Public client key — embedded in window.__SE_BOOTSTRAP and used by the
143
- * browser SDK, and authenticates i18n string fetches (requireKey("client")).
144
- * Safe to expose (e.g. NEXT_PUBLIC_ env vars). If omitted, i18n loading is
145
- * skipped and an error is logged — it is NOT substituted with apiKey (a
146
- * server key 401s against /sdk/i18n/strings).
147
- */
148
- clientKey?: string;
143
+ serverKey?: string;
149
144
  /** Raw URL or query string for applying ?se_ks_* / ?se_cf_* / ?se_exp_* overrides. */
150
145
  urlOverrides?: string;
151
146
  /** User attributes for flag and experiment evaluation. */
@@ -168,23 +163,24 @@ interface ShipeasyServerHandle {
168
163
  */
169
164
  declare function shipeasy(opts: ShipeasyServerConfig): Promise<ShipeasyServerHandle>;
170
165
  interface BootstrapHtmlOptions {
171
- /** SDK client key */
172
- apiKey: string;
173
- /** i18n profile fed to the loader script. Defaults to "en:prod". */
166
+ /** i18n profile recorded in the bootstrap so the client loader matches SSR. Defaults to "en:prod". */
174
167
  i18nProfile?: string;
175
168
  /** When true, tEl() embeds label markers so the devtools can highlight them. */
176
169
  editLabels?: boolean;
177
170
  }
178
171
  /**
179
- * Returns a vanilla-JS script string for a single <script> tag.
180
- * Handles everything the client needs at startup:
172
+ * Returns a vanilla-JS string for a single inline <script> tag. Handles
173
+ * everything the client needs at startup EXCEPT the key — no SDK key is ever
174
+ * embedded here (the server only knows the server key, which must stay
175
+ * server-side). The browser supplies its own client key via
176
+ * `shipeasy({ clientKey })` from @shipeasy/sdk/client, which also injects the
177
+ * runtime i18n loader. This script emits:
181
178
  * - window.__se_devtools_config (when devtoolsAdminUrl is set)
182
- * - window.__SE_BOOTSTRAP (flags + configs + experiments + i18n + apiKey for auto-init)
183
- * - window.i18n shim from SSR strings (prevents hydration mismatches)
184
- * - dynamic <script> injection for the i18n loader
179
+ * - window.__SE_BOOTSTRAP (flags + configs + experiments + i18n DATA + i18nProfile, NO key)
180
+ * - window.i18n shim from SSR strings (prevents hydration mismatches / FOUC)
181
+ * - devtools overlay loader when ?se / ?se_devtools is present
185
182
  *
186
183
  * Framework-agnostic: set innerHTML on a <script> element, nothing else required.
187
- * Pass null for bootstrap on pages without flag evaluation — client still auto-inits.
188
184
  */
189
185
  declare function getBootstrapHtml(bootstrap: BootstrapPayload | null, i18nData: I18nForRequest | null, opts: BootstrapHtmlOptions): string;
190
186
  declare const flags: {
@@ -387,6 +387,7 @@ var _I18N_SSR_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:ssr-i18n");
387
387
  var _EDIT_MODE_SSR_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:ssr-edit-mode");
388
388
  var _i18nALS = new import_node_async_hooks.AsyncLocalStorage();
389
389
  var _I18N_CACHE_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:ssr-i18n-cache");
390
+ var I18N_CACHE_TTL_MS = 6e4;
390
391
  var _i18nCache = globalThis[_I18N_CACHE_SYM] ?? (globalThis[_I18N_CACHE_SYM] = /* @__PURE__ */ new Map());
391
392
  globalThis[_I18N_SSR_SYM] = () => {
392
393
  const fromALS = _i18nALS.getStore();
@@ -432,14 +433,19 @@ var i18n = {
432
433
  const existingALS = _i18nALS.getStore();
433
434
  if (existingALS && Object.keys(existingALS.strings).length > 0) return;
434
435
  const cached = _i18nCache.get(profile);
435
- if (cached && Object.keys(cached.strings).length > 0) {
436
+ if (cached && Object.keys(cached.strings).length > 0 && Date.now() - cached.fetchedAt < I18N_CACHE_TTL_MS) {
436
437
  _i18nALS.enterWith(cached);
437
438
  return;
438
439
  }
439
440
  const labels = await fetchLabelsForSSR({ key, profile, cdnBaseUrl }).catch(() => null);
440
441
  const locale = profile.split(":")[0] || "en";
441
442
  const store = { strings: labels?.strings ?? {}, locale };
442
- if (Object.keys(store.strings).length > 0) _i18nCache.set(profile, store);
443
+ if (Object.keys(store.strings).length > 0) {
444
+ _i18nCache.set(profile, { ...store, fetchedAt: Date.now() });
445
+ } else if (cached && Object.keys(cached.strings).length > 0) {
446
+ _i18nALS.enterWith(cached);
447
+ return;
448
+ }
443
449
  _i18nALS.enterWith(store);
444
450
  },
445
451
  /**
@@ -486,7 +492,6 @@ async function fetchLabelsForSSR(opts) {
486
492
  }
487
493
  }
488
494
  var _server = null;
489
- var _rememberedClientKey = null;
490
495
  function configureShipeasyServer(opts) {
491
496
  if (_server) return _server;
492
497
  _server = new FlagsClient(opts);
@@ -500,21 +505,14 @@ function _resetShipeasyServerForTests() {
500
505
  _server = null;
501
506
  }
502
507
  async function shipeasy(opts) {
503
- const apiKey = opts.apiKey ?? "";
504
- const clientKey = opts.clientKey ?? _rememberedClientKey ?? "";
505
- if (opts.clientKey && !_rememberedClientKey) _rememberedClientKey = opts.clientKey;
506
- if (!apiKey) {
507
- console.error(
508
- "[shipeasy] No server key \u2014 flags & experiments skipped. Pass `apiKey` to shipeasy() with your server key (SHIPEASY_SERVER_KEY). Set it as a Worker secret with `wrangler secret put SHIPEASY_SERVER_KEY` (or add it to .env for local dev). Do not pass a client key here \u2014 /sdk/flags requires a server key and will 401."
509
- );
510
- }
511
- if (!clientKey) {
508
+ const serverKey = opts.serverKey ?? "";
509
+ if (!serverKey) {
512
510
  console.error(
513
- "[shipeasy] No client key \u2014 i18n strings skipped, falling back to hardcoded text. Pass `clientKey` to shipeasy() with your public client key (NEXT_PUBLIC_SHIPEASY_CLIENT_KEY). Do not pass a server key here \u2014 /sdk/i18n/strings requires a client key and will 401."
511
+ "[shipeasy] No server key \u2014 flags, experiments and SSR i18n skipped. Pass `serverKey` to shipeasy() from @shipeasy/sdk/server with your server key (SHIPEASY_SERVER_KEY). Set it as a Worker secret with `wrangler secret put SHIPEASY_SERVER_KEY` (or add it to .env for local dev). Do not pass a client key here \u2014 the server entrypoint only accepts the server key."
514
512
  );
515
513
  }
516
514
  const profile = opts.i18nDefaultProfile ?? "en:prod";
517
- flags.configure({ apiKey });
515
+ flags.configure({ apiKey: serverKey });
518
516
  let resolvedUrlOverrides = opts.urlOverrides;
519
517
  if (!resolvedUrlOverrides) {
520
518
  try {
@@ -535,8 +533,8 @@ async function shipeasy(opts) {
535
533
  const editLabels = resolvedUrlOverrides ? new URLSearchParams(resolvedUrlOverrides).has("se_edit_labels") : false;
536
534
  globalThis[_EDIT_MODE_SSR_SYM] = editLabels;
537
535
  await Promise.allSettled([
538
- apiKey ? flags.initOnce() : Promise.resolve(),
539
- clientKey ? i18n.init(clientKey, profile) : Promise.resolve()
536
+ serverKey ? flags.initOnce() : Promise.resolve(),
537
+ serverKey ? i18n.init(serverKey, profile) : Promise.resolve()
540
538
  ]);
541
539
  const bootstrap = flags.evaluate(opts.user ?? {}, resolvedUrlOverrides);
542
540
  const i18nData = i18n.getForRequest();
@@ -546,7 +544,6 @@ async function shipeasy(opts) {
546
544
  experiments: bootstrap.experiments,
547
545
  getBootstrapHtml() {
548
546
  return getBootstrapHtml(bootstrap, i18nData, {
549
- apiKey: clientKey,
550
547
  editLabels,
551
548
  i18nProfile: profile
552
549
  });
@@ -561,7 +558,9 @@ function getBootstrapHtml(bootstrap, i18nData, opts) {
561
558
  flags: bootstrap?.flags ?? {},
562
559
  configs: bootstrap?.configs ?? {},
563
560
  experiments: bootstrap?.experiments ?? {},
564
- apiKey: opts.apiKey,
561
+ // No key here — the server only knows the server key, which must never reach
562
+ // the browser. The client supplies its own client key via shipeasy({ clientKey }).
563
+ i18nProfile: profile,
565
564
  apiUrl
566
565
  };
567
566
  if (i18nData) payload.i18n = i18nData;
@@ -575,9 +574,6 @@ function getBootstrapHtml(bootstrap, i18nData, opts) {
575
574
  `(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(){};}};})();`
576
575
  );
577
576
  }
578
- parts.push(
579
- `(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);})();`
580
- );
581
577
  parts.push(
582
578
  `(function(){var p=new URLSearchParams(location.search);if(p.has('se')||p.has('se_devtools')){var d=document.createElement('script');d.src='https://shipeasy.ai/se-devtools.js';document.head.appendChild(d);}})();`
583
579
  );
@@ -344,6 +344,7 @@ var _I18N_SSR_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:ssr-i18n");
344
344
  var _EDIT_MODE_SSR_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:ssr-edit-mode");
345
345
  var _i18nALS = new AsyncLocalStorage();
346
346
  var _I18N_CACHE_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:ssr-i18n-cache");
347
+ var I18N_CACHE_TTL_MS = 6e4;
347
348
  var _i18nCache = globalThis[_I18N_CACHE_SYM] ?? (globalThis[_I18N_CACHE_SYM] = /* @__PURE__ */ new Map());
348
349
  globalThis[_I18N_SSR_SYM] = () => {
349
350
  const fromALS = _i18nALS.getStore();
@@ -389,14 +390,19 @@ var i18n = {
389
390
  const existingALS = _i18nALS.getStore();
390
391
  if (existingALS && Object.keys(existingALS.strings).length > 0) return;
391
392
  const cached = _i18nCache.get(profile);
392
- if (cached && Object.keys(cached.strings).length > 0) {
393
+ if (cached && Object.keys(cached.strings).length > 0 && Date.now() - cached.fetchedAt < I18N_CACHE_TTL_MS) {
393
394
  _i18nALS.enterWith(cached);
394
395
  return;
395
396
  }
396
397
  const labels = await fetchLabelsForSSR({ key, profile, cdnBaseUrl }).catch(() => null);
397
398
  const locale = profile.split(":")[0] || "en";
398
399
  const store = { strings: labels?.strings ?? {}, locale };
399
- if (Object.keys(store.strings).length > 0) _i18nCache.set(profile, store);
400
+ if (Object.keys(store.strings).length > 0) {
401
+ _i18nCache.set(profile, { ...store, fetchedAt: Date.now() });
402
+ } else if (cached && Object.keys(cached.strings).length > 0) {
403
+ _i18nALS.enterWith(cached);
404
+ return;
405
+ }
400
406
  _i18nALS.enterWith(store);
401
407
  },
402
408
  /**
@@ -443,7 +449,6 @@ async function fetchLabelsForSSR(opts) {
443
449
  }
444
450
  }
445
451
  var _server = null;
446
- var _rememberedClientKey = null;
447
452
  function configureShipeasyServer(opts) {
448
453
  if (_server) return _server;
449
454
  _server = new FlagsClient(opts);
@@ -457,21 +462,14 @@ function _resetShipeasyServerForTests() {
457
462
  _server = null;
458
463
  }
459
464
  async function shipeasy(opts) {
460
- const apiKey = opts.apiKey ?? "";
461
- const clientKey = opts.clientKey ?? _rememberedClientKey ?? "";
462
- if (opts.clientKey && !_rememberedClientKey) _rememberedClientKey = opts.clientKey;
463
- if (!apiKey) {
464
- console.error(
465
- "[shipeasy] No server key \u2014 flags & experiments skipped. Pass `apiKey` to shipeasy() with your server key (SHIPEASY_SERVER_KEY). Set it as a Worker secret with `wrangler secret put SHIPEASY_SERVER_KEY` (or add it to .env for local dev). Do not pass a client key here \u2014 /sdk/flags requires a server key and will 401."
466
- );
467
- }
468
- if (!clientKey) {
465
+ const serverKey = opts.serverKey ?? "";
466
+ if (!serverKey) {
469
467
  console.error(
470
- "[shipeasy] No client key \u2014 i18n strings skipped, falling back to hardcoded text. Pass `clientKey` to shipeasy() with your public client key (NEXT_PUBLIC_SHIPEASY_CLIENT_KEY). Do not pass a server key here \u2014 /sdk/i18n/strings requires a client key and will 401."
468
+ "[shipeasy] No server key \u2014 flags, experiments and SSR i18n skipped. Pass `serverKey` to shipeasy() from @shipeasy/sdk/server with your server key (SHIPEASY_SERVER_KEY). Set it as a Worker secret with `wrangler secret put SHIPEASY_SERVER_KEY` (or add it to .env for local dev). Do not pass a client key here \u2014 the server entrypoint only accepts the server key."
471
469
  );
472
470
  }
473
471
  const profile = opts.i18nDefaultProfile ?? "en:prod";
474
- flags.configure({ apiKey });
472
+ flags.configure({ apiKey: serverKey });
475
473
  let resolvedUrlOverrides = opts.urlOverrides;
476
474
  if (!resolvedUrlOverrides) {
477
475
  try {
@@ -492,8 +490,8 @@ async function shipeasy(opts) {
492
490
  const editLabels = resolvedUrlOverrides ? new URLSearchParams(resolvedUrlOverrides).has("se_edit_labels") : false;
493
491
  globalThis[_EDIT_MODE_SSR_SYM] = editLabels;
494
492
  await Promise.allSettled([
495
- apiKey ? flags.initOnce() : Promise.resolve(),
496
- clientKey ? i18n.init(clientKey, profile) : Promise.resolve()
493
+ serverKey ? flags.initOnce() : Promise.resolve(),
494
+ serverKey ? i18n.init(serverKey, profile) : Promise.resolve()
497
495
  ]);
498
496
  const bootstrap = flags.evaluate(opts.user ?? {}, resolvedUrlOverrides);
499
497
  const i18nData = i18n.getForRequest();
@@ -503,7 +501,6 @@ async function shipeasy(opts) {
503
501
  experiments: bootstrap.experiments,
504
502
  getBootstrapHtml() {
505
503
  return getBootstrapHtml(bootstrap, i18nData, {
506
- apiKey: clientKey,
507
504
  editLabels,
508
505
  i18nProfile: profile
509
506
  });
@@ -518,7 +515,9 @@ function getBootstrapHtml(bootstrap, i18nData, opts) {
518
515
  flags: bootstrap?.flags ?? {},
519
516
  configs: bootstrap?.configs ?? {},
520
517
  experiments: bootstrap?.experiments ?? {},
521
- apiKey: opts.apiKey,
518
+ // No key here — the server only knows the server key, which must never reach
519
+ // the browser. The client supplies its own client key via shipeasy({ clientKey }).
520
+ i18nProfile: profile,
522
521
  apiUrl
523
522
  };
524
523
  if (i18nData) payload.i18n = i18nData;
@@ -532,9 +531,6 @@ function getBootstrapHtml(bootstrap, i18nData, opts) {
532
531
  `(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(){};}};})();`
533
532
  );
534
533
  }
535
- parts.push(
536
- `(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);})();`
537
- );
538
534
  parts.push(
539
535
  `(function(){var p=new URLSearchParams(location.search);if(p.has('se')||p.has('se_devtools')){var d=document.createElement('script');d.src='https://shipeasy.ai/se-devtools.js';document.head.appendChild(d);}})();`
540
536
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipeasy/sdk",
3
- "version": "2.5.2",
3
+ "version": "3.0.1",
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",