@shipeasy/sdk 5.0.0 → 5.2.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.
@@ -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
  import { AsyncLocalStorage } from "async_hooks";
3
10
 
@@ -315,6 +322,9 @@ var SeeLimiter = class {
315
322
  var version = "4.0.0";
316
323
  var C1 = 3432918353;
317
324
  var C2 = 461845907;
325
+ function _murmur3ForTests(key) {
326
+ return murmur3(key);
327
+ }
318
328
  function murmur3(key) {
319
329
  const bytes = new TextEncoder().encode(key);
320
330
  const len = bytes.length;
@@ -355,6 +365,25 @@ function murmur3(key) {
355
365
  h1 ^= h1 >>> 16;
356
366
  return h1 >>> 0;
357
367
  }
368
+ var FLAG_REASONS = [
369
+ "CLIENT_NOT_READY",
370
+ "FLAG_NOT_FOUND",
371
+ "OFF",
372
+ "OVERRIDE",
373
+ "RULE_MATCH",
374
+ "DEFAULT"
375
+ ];
376
+ function createInMemoryStickyStore(seed) {
377
+ const m = new Map(Object.entries(seed ?? {}));
378
+ return {
379
+ get: (unit) => m.get(unit),
380
+ set: (unit, exp, entry) => {
381
+ const cur = m.get(unit) ?? {};
382
+ cur[exp] = entry;
383
+ m.set(unit, cur);
384
+ }
385
+ };
386
+ }
358
387
  function isEnabled(v) {
359
388
  return v === 1 || v === true;
360
389
  }
@@ -407,6 +436,14 @@ var ANON_ID_RX = /^[A-Za-z0-9_-]{1,64}$/;
407
436
  function mintAnonId() {
408
437
  return typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : `anon_${Math.random().toString(36).slice(2)}`;
409
438
  }
439
+ function pickIdentifier(user, bucketBy) {
440
+ if (bucketBy) {
441
+ const v = user[bucketBy];
442
+ if (typeof v === "string" && v.length > 0) return v;
443
+ if (typeof v === "number") return String(v);
444
+ }
445
+ return user.user_id ?? user.anonymous_id;
446
+ }
410
447
  function evalGateInternal(gate, user) {
411
448
  if (isEnabled(gate.killswitch)) return false;
412
449
  if (!isEnabled(gate.enabled)) return false;
@@ -462,10 +499,12 @@ function parseOverrides(rawUrl) {
462
499
  }
463
500
  return { gates, configs, experiments };
464
501
  }
465
- var FlagsClient = class {
502
+ var FlagsClient = class _FlagsClient {
466
503
  apiKey;
467
504
  baseUrl;
468
505
  env;
506
+ privateAttributes;
507
+ stickyStore;
469
508
  telemetry;
470
509
  seeLimiter = new SeeLimiter();
471
510
  flagsBlob = null;
@@ -475,47 +514,157 @@ var FlagsClient = class {
475
514
  pollInterval = 30;
476
515
  timer = null;
477
516
  initialized = false;
517
+ // Test mode: built by `FlagsClient.forTesting()`. When set, init()/initOnce()
518
+ // never fetch, track() is a no-op, and telemetry is off — the client is a
519
+ // fully self-contained, network-free seam for unit tests.
520
+ testMode;
521
+ // Programmatic overrides (Statsig-style). Set on any client via
522
+ // overrideFlag/overrideConfig/overrideExperiment; they win over the fetched
523
+ // blob in getFlag/getConfig/getExperiment. Cleared by clearOverrides().
524
+ flagOverrides = /* @__PURE__ */ new Map();
525
+ configOverrides = /* @__PURE__ */ new Map();
526
+ experimentOverrides = /* @__PURE__ */ new Map();
527
+ // Change listeners fired after a background poll returns NEW data (200, not
528
+ // 304). Never fired in testMode/offline (no polling happens there).
529
+ changeListeners = /* @__PURE__ */ new Set();
478
530
  constructor(opts) {
479
531
  this.apiKey = opts.apiKey;
480
532
  this.baseUrl = (opts.baseUrl ?? "https://cdn.shipeasy.ai").replace(/\/$/, "");
481
533
  this.env = opts.env ?? "prod";
534
+ this.privateAttributes = opts.privateAttributes ?? [];
535
+ this.stickyStore = opts.stickyStore;
536
+ this.testMode = opts.testMode === true;
482
537
  this.telemetry = new Telemetry({
483
538
  endpoint: opts.telemetryUrl ?? DEFAULT_TELEMETRY_URL,
484
539
  sdkKey: this.apiKey,
485
540
  side: "server",
486
541
  env: this.env,
487
- disabled: opts.disableTelemetry
542
+ // Test mode never talks to the network — telemetry off regardless of opt.
543
+ disabled: this.testMode || opts.disableTelemetry
488
544
  });
489
- if (opts.initialBlob) {
490
- this.flagsBlob = opts.initialBlob;
545
+ if (opts.initialBlob || this.testMode) {
546
+ this.flagsBlob = opts.initialBlob ?? { version: "test", plan: "free", gates: {}, configs: {}, killswitches: {} };
547
+ this.expsBlob = this.expsBlob ?? { version: "test", universes: {}, experiments: {} };
491
548
  this.initialized = true;
492
549
  }
493
550
  }
551
+ /**
552
+ * Build a no-network, immediately-usable client for tests (Statsig-style).
553
+ * init()/initOnce() are no-ops (never fetch), track() is a no-op, telemetry
554
+ * is disabled, and the client is already "initialized" — seed every entity
555
+ * with overrideFlag/overrideConfig/overrideExperiment. No SDK key required.
556
+ *
557
+ * ```ts
558
+ * const client = FlagsClient.forTesting();
559
+ * client.overrideFlag("new_checkout", true);
560
+ * client.getFlag("new_checkout", { user_id: "u1" }); // true
561
+ * ```
562
+ */
563
+ static forTesting(opts) {
564
+ return new _FlagsClient({ apiKey: "", ...opts, testMode: true });
565
+ }
566
+ /**
567
+ * Build a fully OFFLINE client from a pre-captured snapshot — no network ever.
568
+ * Reuses the test-mode plumbing (init()/initOnce()/track() are no-ops,
569
+ * telemetry off) but, unlike forTesting(), seeds the REAL flags + experiments
570
+ * blobs so evaluations run the canonical eval against the snapshot. Local
571
+ * overrides still apply on top.
572
+ *
573
+ * Snapshot shape mirrors the wire bodies:
574
+ * `{ flags: <GET /sdk/flags body>, experiments: <GET /sdk/experiments body> }`.
575
+ */
576
+ static fromSnapshot(snapshot) {
577
+ const client = new _FlagsClient({ apiKey: "", testMode: true });
578
+ client.flagsBlob = snapshot.flags;
579
+ client.expsBlob = snapshot.experiments;
580
+ client.initialized = true;
581
+ return client;
582
+ }
583
+ /**
584
+ * Build a fully OFFLINE client from a snapshot JSON file on disk (Node only —
585
+ * not available in the browser entrypoint). The file must contain
586
+ * `{ "flags": <GET /sdk/flags body>, "experiments": <GET /sdk/experiments body> }`.
587
+ * See {@link FlagsClient.fromSnapshot}.
588
+ */
589
+ static fromFile(path) {
590
+ const fs = __require("fs");
591
+ const raw = fs.readFileSync(path, "utf8");
592
+ const snapshot = JSON.parse(raw);
593
+ return _FlagsClient.fromSnapshot(snapshot);
594
+ }
494
595
  async init() {
596
+ if (this.testMode) {
597
+ this.initialized = true;
598
+ return;
599
+ }
495
600
  await this.fetchAll();
496
601
  this.initialized = true;
497
602
  this.startPoll();
498
603
  }
499
604
  async initOnce() {
500
- if (this.initialized) return;
605
+ if (this.testMode || this.initialized) return;
501
606
  await this.fetchAll();
502
607
  this.initialized = true;
503
608
  }
609
+ // ---- Local overrides (Statsig-style) ----
610
+ /** Force `getFlag(name, …)` to return `value`, ignoring the fetched gate. */
611
+ overrideFlag(name, value) {
612
+ this.flagOverrides.set(name, value);
613
+ }
614
+ /** Force `getConfig(name)` to return `value`, ignoring the fetched config. */
615
+ overrideConfig(name, value) {
616
+ this.configOverrides.set(name, value);
617
+ }
618
+ /**
619
+ * Force `getExperiment(name, …)` to return `{ inExperiment: true, group, params }`,
620
+ * ignoring allocation, holdouts, and targeting.
621
+ */
622
+ overrideExperiment(name, group, params) {
623
+ this.experimentOverrides.set(name, { group, params });
624
+ }
625
+ /** Remove every programmatic override set via the override* methods. */
626
+ clearOverrides() {
627
+ this.flagOverrides.clear();
628
+ this.configOverrides.clear();
629
+ this.experimentOverrides.clear();
630
+ }
504
631
  destroy() {
505
632
  if (this.timer !== null) {
506
633
  clearInterval(this.timer);
507
634
  this.timer = null;
508
635
  }
509
636
  }
637
+ /**
638
+ * Subscribe to data-change notifications. The listener fires after a
639
+ * background poll fetch returns NEW data (200, not 304) — i.e. after the
640
+ * cached blob is updated. Never fires in testMode/offline (no polling).
641
+ * Returns an unsubscribe function.
642
+ */
643
+ onChange(listener) {
644
+ this.changeListeners.add(listener);
645
+ return () => {
646
+ this.changeListeners.delete(listener);
647
+ };
648
+ }
649
+ notifyChange() {
650
+ for (const l of this.changeListeners) {
651
+ try {
652
+ l();
653
+ } catch (err) {
654
+ console.warn("[shipeasy] onChange listener threw:", String(err));
655
+ }
656
+ }
657
+ }
510
658
  startPoll() {
511
659
  this.timer = setInterval(() => {
512
- this.fetchAll().catch(
660
+ this.fetchAll(true).catch(
513
661
  (err) => console.warn("[shipeasy] background poll failed:", String(err))
514
662
  );
515
663
  }, this.pollInterval * 1e3);
516
664
  }
517
- async fetchAll() {
518
- const [interval] = await Promise.all([this.fetchFlags(), this.fetchExps()]);
665
+ async fetchAll(fromPoll = false) {
666
+ const [flagsRes, expsChanged] = await Promise.all([this.fetchFlags(), this.fetchExps()]);
667
+ const { interval, changed: flagsChanged } = flagsRes;
519
668
  if (interval !== null && interval !== this.pollInterval) {
520
669
  this.pollInterval = interval;
521
670
  if (this.timer !== null) {
@@ -523,41 +672,72 @@ var FlagsClient = class {
523
672
  this.startPoll();
524
673
  }
525
674
  }
675
+ if (fromPoll && (flagsChanged || expsChanged)) this.notifyChange();
526
676
  }
527
677
  async fetchFlags() {
528
678
  const headers = { "X-SDK-Key": this.apiKey };
529
679
  if (this.flagsEtag) headers["If-None-Match"] = this.flagsEtag;
530
680
  const res = await globalThis.fetch(`${this.baseUrl}/sdk/flags?env=${this.env}`, { headers });
531
681
  const interval = Number(res.headers.get("X-Poll-Interval") ?? "30") || 30;
532
- if (res.status === 304) return interval;
682
+ if (res.status === 304) return { interval, changed: false };
533
683
  if (!res.ok) throw new Error(`/sdk/flags returned ${res.status}`);
534
684
  const etag = res.headers.get("ETag");
535
685
  if (etag) this.flagsEtag = etag;
536
686
  this.flagsBlob = await res.json();
537
- return interval;
687
+ return { interval, changed: true };
538
688
  }
539
689
  async fetchExps() {
540
690
  const headers = { "X-SDK-Key": this.apiKey };
541
691
  if (this.expsEtag) headers["If-None-Match"] = this.expsEtag;
542
692
  const res = await globalThis.fetch(`${this.baseUrl}/sdk/experiments`, { headers });
543
- if (res.status === 304) return;
693
+ if (res.status === 304) return false;
544
694
  if (!res.ok) throw new Error(`/sdk/experiments returned ${res.status}`);
545
695
  const etag = res.headers.get("ETag");
546
696
  if (etag) this.expsEtag = etag;
547
697
  this.expsBlob = await res.json();
698
+ return true;
548
699
  }
549
- getFlag(name, user) {
700
+ /**
701
+ * Evaluate a gate and report WHY (LaunchDarkly variationDetail parity). The
702
+ * reason is computed entirely at this boundary — the canonical eval
703
+ * (evalGateInternal) is untouched. A local override short-circuits BEFORE
704
+ * telemetry, exactly like getFlag's override path; otherwise exactly one
705
+ * "gate" beacon is emitted.
706
+ */
707
+ getFlagDetail(name, user) {
708
+ const ov = this.flagOverrides.get(name);
709
+ if (ov !== void 0) return { value: ov, reason: "OVERRIDE" };
550
710
  this.telemetry.emit("gate", name);
551
- const gate = this.flagsBlob?.gates[name];
552
- if (!gate) return false;
553
- return evalGateInternal(gate, user);
711
+ if (!this.flagsBlob) return { value: false, reason: "CLIENT_NOT_READY" };
712
+ const gate = this.flagsBlob.gates[name];
713
+ if (!gate) return { value: false, reason: "FLAG_NOT_FOUND" };
714
+ if (isEnabled(gate.killswitch) || !isEnabled(gate.enabled)) {
715
+ return { value: false, reason: "OFF" };
716
+ }
717
+ const value = evalGateInternal(gate, user);
718
+ return { value, reason: value ? "RULE_MATCH" : "DEFAULT" };
554
719
  }
555
- getConfig(name, decode) {
720
+ /**
721
+ * Read a feature gate. Returns `defaultValue` ONLY when the gate cannot be
722
+ * evaluated (client not initialized or flag not found) — never for a gate that
723
+ * legitimately evaluates to false. Plain `getFlag(name, user)` keeps returning
724
+ * false for a missing flag.
725
+ */
726
+ getFlag(name, user, defaultValue = false) {
727
+ const d = this.getFlagDetail(name, user);
728
+ if (d.reason === "CLIENT_NOT_READY" || d.reason === "FLAG_NOT_FOUND") return defaultValue;
729
+ return d.value;
730
+ }
731
+ getConfig(name, decodeOrOpts) {
556
732
  this.telemetry.emit("config", name);
557
- const entry = this.flagsBlob?.configs[name];
558
- if (!entry) return void 0;
559
- if (!decode) return entry.value;
560
- return decode(entry.value);
733
+ const opts = typeof decodeOrOpts === "function" ? { decode: decodeOrOpts } : decodeOrOpts ?? {};
734
+ const has = this.configOverrides.has(name);
735
+ const raw = has ? this.configOverrides.get(name) : this.flagsBlob?.configs[name]?.value;
736
+ if (raw === void 0 && !has) {
737
+ return "defaultValue" in opts ? opts.defaultValue : void 0;
738
+ }
739
+ if (!opts.decode) return raw;
740
+ return opts.decode(raw);
561
741
  }
562
742
  getExperiment(name, user, defaultParams, decode) {
563
743
  this.telemetry.emit("experiment", name);
@@ -566,6 +746,16 @@ var FlagsClient = class {
566
746
  group: "control",
567
747
  params: defaultParams
568
748
  };
749
+ const ov = this.experimentOverrides.get(name);
750
+ if (ov) {
751
+ if (!decode) return { inExperiment: true, group: ov.group, params: ov.params };
752
+ try {
753
+ return { inExperiment: true, group: ov.group, params: decode(ov.params) };
754
+ } catch (err) {
755
+ console.warn(`[shipeasy] getExperiment('${name}') override decode failed:`, String(err));
756
+ return notIn;
757
+ }
758
+ }
569
759
  if (!this.flagsBlob || !this.expsBlob) return notIn;
570
760
  const exp = this.expsBlob.experiments[name];
571
761
  if (!exp || exp.status !== "running") return notIn;
@@ -573,7 +763,7 @@ var FlagsClient = class {
573
763
  const gate = this.flagsBlob.gates[exp.targetingGate];
574
764
  if (!gate || !evalGateInternal(gate, user)) return notIn;
575
765
  }
576
- const uid = user.user_id ?? user.anonymous_id;
766
+ const uid = pickIdentifier(user, exp.bucketBy);
577
767
  if (!uid) return notIn;
578
768
  const universe = this.expsBlob.universes[exp.universe];
579
769
  const holdoutRange = universe?.holdout_range ?? null;
@@ -582,6 +772,23 @@ var FlagsClient = class {
582
772
  const [lo, hi] = holdoutRange;
583
773
  if (seg >= lo && seg <= hi) return notIn;
584
774
  }
775
+ const asResult = (g) => {
776
+ if (!decode) return { inExperiment: true, group: g.name, params: g.params };
777
+ try {
778
+ return { inExperiment: true, group: g.name, params: decode(g.params) };
779
+ } catch (err) {
780
+ console.warn(`[shipeasy] getExperiment('${name}') decode failed:`, String(err));
781
+ return notIn;
782
+ }
783
+ };
784
+ const salt8 = exp.salt.slice(0, 8);
785
+ if (this.stickyStore) {
786
+ const entry = this.stickyStore.get(uid)?.[name];
787
+ if (entry && entry.s === salt8) {
788
+ const g = exp.groups.find((x) => x.name === entry.g);
789
+ if (g) return asResult(g);
790
+ }
791
+ }
585
792
  if (murmur3(`${exp.salt}:alloc:${uid}`) % 1e4 >= exp.allocationPct) return notIn;
586
793
  const groupHash = murmur3(`${exp.salt}:group:${uid}`) % 1e4;
587
794
  let cumulative = 0;
@@ -589,20 +796,24 @@ var FlagsClient = class {
589
796
  const g = exp.groups[i];
590
797
  cumulative += g.weight;
591
798
  if (groupHash < cumulative || i === exp.groups.length - 1) {
592
- if (!decode) {
593
- return { inExperiment: true, group: g.name, params: g.params };
594
- }
595
- try {
596
- return { inExperiment: true, group: g.name, params: decode(g.params) };
597
- } catch (err) {
598
- console.warn(`[shipeasy] getExperiment('${name}') decode failed:`, String(err));
599
- return notIn;
600
- }
799
+ this.stickyStore?.set(uid, name, { g: g.name, s: salt8 });
800
+ return asResult(g);
601
801
  }
602
802
  }
603
803
  return notIn;
604
804
  }
805
+ /** Drop caller-marked private attributes from an outbound props bag. */
806
+ stripPrivate(props) {
807
+ if (!props || this.privateAttributes.length === 0) return props;
808
+ const out = {};
809
+ for (const [k, v] of Object.entries(props)) {
810
+ if (!this.privateAttributes.includes(k)) out[k] = v;
811
+ }
812
+ return out;
813
+ }
605
814
  track(userId, eventName, props) {
815
+ if (this.testMode) return;
816
+ const safeProps = this.stripPrivate(props);
606
817
  const body = JSON.stringify({
607
818
  events: [
608
819
  {
@@ -610,7 +821,7 @@ var FlagsClient = class {
610
821
  event_name: eventName,
611
822
  user_id: userId,
612
823
  ts: Date.now(),
613
- ...props !== void 0 ? { properties: props } : {}
824
+ ...safeProps !== void 0 ? { properties: safeProps } : {}
614
825
  }
615
826
  ]
616
827
  });
@@ -620,6 +831,37 @@ var FlagsClient = class {
620
831
  body
621
832
  }).catch((err) => console.warn("[shipeasy] track failed:", String(err)));
622
833
  }
834
+ /**
835
+ * Emit an exposure event for an experiment at the server-side decision point
836
+ * (parity with the browser's auto-exposure). The server is stateless and
837
+ * never auto-logs, so call this when you actually present the treatment.
838
+ * Re-evaluates the experiment for `user` (a bare `user_id` string is wrapped
839
+ * as `{ user_id }`); if the user is enrolled, POSTs a single exposure to
840
+ * `/collect`. No-op in test mode or when the user isn't enrolled.
841
+ */
842
+ logExposure(user, name) {
843
+ if (this.testMode) return;
844
+ const u = typeof user === "string" ? { user_id: user } : user;
845
+ const result = this.getExperiment(name, u, {});
846
+ if (!result.inExperiment) return;
847
+ const body = JSON.stringify({
848
+ events: [
849
+ {
850
+ type: "exposure",
851
+ experiment: name,
852
+ group: result.group,
853
+ ...u.user_id !== void 0 ? { user_id: u.user_id } : {},
854
+ ...u.anonymous_id !== void 0 ? { anonymous_id: u.anonymous_id } : {},
855
+ ts: Date.now()
856
+ }
857
+ ]
858
+ });
859
+ globalThis.fetch(`${this.baseUrl}/collect`, {
860
+ method: "POST",
861
+ headers: { "X-SDK-Key": this.apiKey, "Content-Type": "text/plain" },
862
+ body
863
+ }).catch((err) => console.warn("[shipeasy] logExposure failed:", String(err)));
864
+ }
623
865
  /**
624
866
  * Report a structured error into the errors primitive. Fire-and-forget —
625
867
  * never blocks or throws into the request path. Spam-guarded by a 30s
@@ -826,7 +1068,11 @@ async function shipeasy(opts) {
826
1068
  );
827
1069
  }
828
1070
  const profile = opts.i18nDefaultProfile ?? "en:prod";
829
- flags.configure({ apiKey: serverKey, disableTelemetry: opts.disableTelemetry });
1071
+ flags.configure({
1072
+ apiKey: serverKey,
1073
+ disableTelemetry: opts.disableTelemetry,
1074
+ privateAttributes: opts.privateAttributes
1075
+ });
830
1076
  let resolvedUrlOverrides = opts.urlOverrides;
831
1077
  if (!resolvedUrlOverrides) {
832
1078
  try {
@@ -937,11 +1183,15 @@ var flags = {
937
1183
  destroy() {
938
1184
  _server?.destroy();
939
1185
  },
940
- get(name, user) {
941
- return _server?.getFlag(name, user) ?? false;
1186
+ get(name, user, defaultValue = false) {
1187
+ return _server?.getFlag(name, user, defaultValue) ?? defaultValue;
942
1188
  },
943
- getConfig(name, decode) {
944
- return _server?.getConfig(name, decode);
1189
+ /** Evaluate a gate and report why (value + reason). See {@link FlagDetail}. */
1190
+ getDetail(name, user) {
1191
+ return _server?.getFlagDetail(name, user) ?? { value: false, reason: "CLIENT_NOT_READY" };
1192
+ },
1193
+ getConfig(name, decodeOrOpts) {
1194
+ return _server?.getConfig(name, decodeOrOpts);
945
1195
  },
946
1196
  getExperiment(name, user, defaultParams, decode) {
947
1197
  return _server?.getExperiment(name, user, defaultParams, decode) ?? {
@@ -961,6 +1211,11 @@ var flags = {
961
1211
  track(userId, eventName, props) {
962
1212
  _server?.track(userId, eventName, props);
963
1213
  },
1214
+ /** Emit an exposure for an enrolled experiment at the decision point. See
1215
+ * {@link FlagsClient.logExposure}. No-op before configure(). */
1216
+ logExposure(user, name) {
1217
+ _server?.logExposure(user, name);
1218
+ },
964
1219
  /**
965
1220
  * Evaluate all flags / configs / experiments for a user against the locally
966
1221
  * cached blob. Pass the request URL to apply ?se_ks_* / ?se_cf_* / ?se_exp_*
@@ -991,9 +1246,12 @@ var see = Object.assign(
991
1246
  );
992
1247
  export {
993
1248
  ANON_ID_COOKIE,
1249
+ FLAG_REASONS,
994
1250
  FlagsClient,
1251
+ _murmur3ForTests,
995
1252
  _resetShipeasyServerForTests,
996
1253
  configureShipeasyServer,
1254
+ createInMemoryStickyStore,
997
1255
  fetchLabelsForSSR,
998
1256
  flags,
999
1257
  getBootstrapHtml,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipeasy/sdk",
3
- "version": "5.0.0",
3
+ "version": "5.2.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",
@@ -47,12 +47,24 @@
47
47
  "import": "./dist/next/index.mjs",
48
48
  "default": "./dist/next/index.js"
49
49
  },
50
+ "./openfeature-server": {
51
+ "types": "./dist/openfeature-server/index.d.ts",
52
+ "import": "./dist/openfeature-server/index.mjs",
53
+ "default": "./dist/openfeature-server/index.js"
54
+ },
55
+ "./openfeature-web": {
56
+ "types": "./dist/openfeature-web/index.d.ts",
57
+ "import": "./dist/openfeature-web/index.mjs",
58
+ "default": "./dist/openfeature-web/index.js"
59
+ },
50
60
  "./templates/*": "./templates/*.js"
51
61
  },
52
62
  "files": [
53
63
  "dist/server/",
54
64
  "dist/client/",
55
65
  "dist/next/",
66
+ "dist/openfeature-server/",
67
+ "dist/openfeature-web/",
56
68
  "templates/",
57
69
  "LICENSE",
58
70
  "README.md"
@@ -68,14 +80,24 @@
68
80
  "murmurhash-js": "^1.0.0"
69
81
  },
70
82
  "peerDependencies": {
83
+ "@openfeature/server-sdk": ">=1.13.0",
84
+ "@openfeature/web-sdk": ">=1.2.0",
71
85
  "next": ">=13"
72
86
  },
73
87
  "peerDependenciesMeta": {
88
+ "@openfeature/server-sdk": {
89
+ "optional": true
90
+ },
91
+ "@openfeature/web-sdk": {
92
+ "optional": true
93
+ },
74
94
  "next": {
75
95
  "optional": true
76
96
  }
77
97
  },
78
98
  "devDependencies": {
99
+ "@openfeature/server-sdk": "^1.22.0",
100
+ "@openfeature/web-sdk": "^1.9.0",
79
101
  "@types/murmurhash-js": "^1.0.6",
80
102
  "@types/node": "^20.0.0",
81
103
  "next": "^16.2.3",