@shipeasy/sdk 4.3.0 → 4.5.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.
@@ -729,17 +729,40 @@ function installAutoGuardrails(buffer, userId, anonId, groups, reportSee, ignore
729
729
  });
730
730
  }
731
731
  }
732
- function getOrCreateAnonId() {
732
+ function readAnonCookie() {
733
733
  try {
734
- const stored = localStorage.getItem(ANON_ID_KEY);
735
- if (stored) return stored;
734
+ const m = ("; " + document.cookie).match(/; __se_anon_id=([^;]+)/);
735
+ return m ? decodeURIComponent(m[1]) : null;
736
736
  } catch {
737
+ return null;
738
+ }
739
+ }
740
+ function writeAnonCookie(id) {
741
+ try {
742
+ const secure = location.protocol === "https:" ? ";secure" : "";
743
+ document.cookie = `${ANON_ID_KEY}=${id};path=/;max-age=31536000;samesite=lax${secure}`;
744
+ } catch {
745
+ }
746
+ }
747
+ function getOrCreateAnonId() {
748
+ let id = readAnonCookie();
749
+ if (!id && typeof window !== "undefined") {
750
+ id = window.__SE_BOOTSTRAP?.anonId ?? null;
751
+ }
752
+ if (!id) {
753
+ try {
754
+ id = localStorage.getItem(ANON_ID_KEY);
755
+ } catch {
756
+ }
757
+ }
758
+ if (!id) {
759
+ id = typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : `anon_${Math.random().toString(36).slice(2)}`;
737
760
  }
738
- const id = typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : `anon_${Math.random().toString(36).slice(2)}`;
739
761
  try {
740
762
  localStorage.setItem(ANON_ID_KEY, id);
741
763
  } catch {
742
764
  }
765
+ writeAnonCookie(id);
743
766
  return id;
744
767
  }
745
768
  function collectBrowserAttrs() {
@@ -681,17 +681,40 @@ function installAutoGuardrails(buffer, userId, anonId, groups, reportSee, ignore
681
681
  });
682
682
  }
683
683
  }
684
- function getOrCreateAnonId() {
684
+ function readAnonCookie() {
685
685
  try {
686
- const stored = localStorage.getItem(ANON_ID_KEY);
687
- if (stored) return stored;
686
+ const m = ("; " + document.cookie).match(/; __se_anon_id=([^;]+)/);
687
+ return m ? decodeURIComponent(m[1]) : null;
688
688
  } catch {
689
+ return null;
690
+ }
691
+ }
692
+ function writeAnonCookie(id) {
693
+ try {
694
+ const secure = location.protocol === "https:" ? ";secure" : "";
695
+ document.cookie = `${ANON_ID_KEY}=${id};path=/;max-age=31536000;samesite=lax${secure}`;
696
+ } catch {
697
+ }
698
+ }
699
+ function getOrCreateAnonId() {
700
+ let id = readAnonCookie();
701
+ if (!id && typeof window !== "undefined") {
702
+ id = window.__SE_BOOTSTRAP?.anonId ?? null;
703
+ }
704
+ if (!id) {
705
+ try {
706
+ id = localStorage.getItem(ANON_ID_KEY);
707
+ } catch {
708
+ }
709
+ }
710
+ if (!id) {
711
+ id = typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : `anon_${Math.random().toString(36).slice(2)}`;
689
712
  }
690
- const id = typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : `anon_${Math.random().toString(36).slice(2)}`;
691
713
  try {
692
714
  localStorage.setItem(ANON_ID_KEY, id);
693
715
  } catch {
694
716
  }
717
+ writeAnonCookie(id);
695
718
  return id;
696
719
  }
697
720
  function collectBrowserAttrs() {
@@ -0,0 +1,50 @@
1
+ import { NextRequest, NextFetchEvent, NextResponse } from 'next/server';
2
+
3
+ /** The first-party cookie that carries the stable anonymous bucketing unit. */
4
+ declare const ANON_ID_COOKIE = "__se_anon_id";
5
+ interface AnonIdResult {
6
+ /** The stable bucketing id for this request (existing cookie, or freshly minted). */
7
+ anonId: string;
8
+ /** True when there was no valid cookie and we minted a new id (must be persisted). */
9
+ minted: boolean;
10
+ }
11
+ /**
12
+ * Read + validate the `__se_anon_id` cookie, minting one when absent/invalid.
13
+ * When `requestHeaders` is supplied and we mint, the new id is appended to the
14
+ * forwarded `cookie` header so THIS request's SSR (and any inner middleware)
15
+ * already sees it. Pair with {@link commitAnonId} to persist a minted id.
16
+ */
17
+ declare function readOrMintAnonId(req: NextRequest, requestHeaders?: Headers): AnonIdResult;
18
+ /**
19
+ * Persist a freshly-minted anon id as a first-party cookie on `res` (no-op when
20
+ * `minted` is false). Non-httpOnly by contract — the browser SDK reads it via
21
+ * `document.cookie` to bucket identically to SSR. Returns `res` for chaining.
22
+ */
23
+ declare function commitAnonId(res: NextResponse, result: AnonIdResult, req: NextRequest): NextResponse;
24
+ type ShipeasyMiddleware = (req: NextRequest, event: NextFetchEvent) => NextResponse | Response | undefined | null | void | Promise<NextResponse | Response | undefined | null | void>;
25
+ /**
26
+ * Wrap an existing Next middleware so every matched request also mints the
27
+ * shared `__se_anon_id` cookie. Your middleware runs against a request that
28
+ * already carries the id (its own logic + SSR see it), and the cookie is set on
29
+ * whatever response it returns. Called with no argument, returns a standalone
30
+ * mint-only middleware (what the default {@link middleware} export uses).
31
+ *
32
+ * If your middleware forwards custom request headers via
33
+ * `NextResponse.next({ request: { headers } })`, prefer composing the
34
+ * {@link readOrMintAnonId} + {@link commitAnonId} primitives inside it — that
35
+ * preserves your forwarding verbatim; this wrapper rebuilds the pass-through
36
+ * `next()` and so would drop request-header forwarding the inner handler added.
37
+ */
38
+ declare function withShipeasy(handler?: ShipeasyMiddleware): ShipeasyMiddleware;
39
+ /** Drop-in middleware: `export { middleware, config } from "@shipeasy/sdk/next";` */
40
+ declare const middleware: ShipeasyMiddleware;
41
+ /**
42
+ * Matcher for the drop-in middleware: every HTML route (landing + app), skipping
43
+ * Next internals, API routes, and static files (any path segment with a dot).
44
+ * Define your own `config` if you need a narrower scope.
45
+ */
46
+ declare const config: {
47
+ matcher: string[];
48
+ };
49
+
50
+ export { ANON_ID_COOKIE, type AnonIdResult, type ShipeasyMiddleware, commitAnonId, config, middleware, readOrMintAnonId, withShipeasy };
@@ -0,0 +1,50 @@
1
+ import { NextRequest, NextFetchEvent, NextResponse } from 'next/server';
2
+
3
+ /** The first-party cookie that carries the stable anonymous bucketing unit. */
4
+ declare const ANON_ID_COOKIE = "__se_anon_id";
5
+ interface AnonIdResult {
6
+ /** The stable bucketing id for this request (existing cookie, or freshly minted). */
7
+ anonId: string;
8
+ /** True when there was no valid cookie and we minted a new id (must be persisted). */
9
+ minted: boolean;
10
+ }
11
+ /**
12
+ * Read + validate the `__se_anon_id` cookie, minting one when absent/invalid.
13
+ * When `requestHeaders` is supplied and we mint, the new id is appended to the
14
+ * forwarded `cookie` header so THIS request's SSR (and any inner middleware)
15
+ * already sees it. Pair with {@link commitAnonId} to persist a minted id.
16
+ */
17
+ declare function readOrMintAnonId(req: NextRequest, requestHeaders?: Headers): AnonIdResult;
18
+ /**
19
+ * Persist a freshly-minted anon id as a first-party cookie on `res` (no-op when
20
+ * `minted` is false). Non-httpOnly by contract — the browser SDK reads it via
21
+ * `document.cookie` to bucket identically to SSR. Returns `res` for chaining.
22
+ */
23
+ declare function commitAnonId(res: NextResponse, result: AnonIdResult, req: NextRequest): NextResponse;
24
+ type ShipeasyMiddleware = (req: NextRequest, event: NextFetchEvent) => NextResponse | Response | undefined | null | void | Promise<NextResponse | Response | undefined | null | void>;
25
+ /**
26
+ * Wrap an existing Next middleware so every matched request also mints the
27
+ * shared `__se_anon_id` cookie. Your middleware runs against a request that
28
+ * already carries the id (its own logic + SSR see it), and the cookie is set on
29
+ * whatever response it returns. Called with no argument, returns a standalone
30
+ * mint-only middleware (what the default {@link middleware} export uses).
31
+ *
32
+ * If your middleware forwards custom request headers via
33
+ * `NextResponse.next({ request: { headers } })`, prefer composing the
34
+ * {@link readOrMintAnonId} + {@link commitAnonId} primitives inside it — that
35
+ * preserves your forwarding verbatim; this wrapper rebuilds the pass-through
36
+ * `next()` and so would drop request-header forwarding the inner handler added.
37
+ */
38
+ declare function withShipeasy(handler?: ShipeasyMiddleware): ShipeasyMiddleware;
39
+ /** Drop-in middleware: `export { middleware, config } from "@shipeasy/sdk/next";` */
40
+ declare const middleware: ShipeasyMiddleware;
41
+ /**
42
+ * Matcher for the drop-in middleware: every HTML route (landing + app), skipping
43
+ * Next internals, API routes, and static files (any path segment with a dot).
44
+ * Define your own `config` if you need a narrower scope.
45
+ */
46
+ declare const config: {
47
+ matcher: string[];
48
+ };
49
+
50
+ export { ANON_ID_COOKIE, type AnonIdResult, type ShipeasyMiddleware, commitAnonId, config, middleware, readOrMintAnonId, withShipeasy };
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/next/index.ts
21
+ var next_exports = {};
22
+ __export(next_exports, {
23
+ ANON_ID_COOKIE: () => ANON_ID_COOKIE,
24
+ commitAnonId: () => commitAnonId,
25
+ config: () => config,
26
+ middleware: () => middleware,
27
+ readOrMintAnonId: () => readOrMintAnonId,
28
+ withShipeasy: () => withShipeasy
29
+ });
30
+ module.exports = __toCommonJS(next_exports);
31
+ var import_server = require("next/server");
32
+ var ANON_ID_COOKIE = "__se_anon_id";
33
+ var ANON_ID_RX = /^[A-Za-z0-9_-]{1,64}$/;
34
+ var ANON_ID_MAX_AGE = 60 * 60 * 24 * 365;
35
+ function mint() {
36
+ return typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : `anon_${Math.random().toString(36).slice(2)}`;
37
+ }
38
+ function appendCookie(headers, name, value) {
39
+ const existing = headers.get("cookie") ?? "";
40
+ const parts = existing.split(";").map((c) => c.trim()).filter((c) => c && !c.startsWith(`${name}=`));
41
+ parts.push(`${name}=${value}`);
42
+ headers.set("cookie", parts.join("; "));
43
+ }
44
+ function readOrMintAnonId(req, requestHeaders) {
45
+ const raw = req.cookies.get(ANON_ID_COOKIE)?.value;
46
+ const valid = !!raw && ANON_ID_RX.test(raw);
47
+ const anonId = valid ? raw : mint();
48
+ const minted = !valid;
49
+ if (minted && requestHeaders) appendCookie(requestHeaders, ANON_ID_COOKIE, anonId);
50
+ return { anonId, minted };
51
+ }
52
+ function commitAnonId(res, result, req) {
53
+ if (result.minted) {
54
+ res.cookies.set(ANON_ID_COOKIE, result.anonId, {
55
+ httpOnly: false,
56
+ secure: req.nextUrl.protocol === "https:",
57
+ sameSite: "lax",
58
+ path: "/",
59
+ maxAge: ANON_ID_MAX_AGE
60
+ });
61
+ }
62
+ return res;
63
+ }
64
+ function withShipeasy(handler) {
65
+ return async (req, event) => {
66
+ const requestHeaders = new Headers(req.headers);
67
+ const anon = readOrMintAnonId(req, requestHeaders);
68
+ if (anon.minted) req.cookies.set(ANON_ID_COOKIE, anon.anonId);
69
+ const userRes = handler ? await handler(req, event) : void 0;
70
+ const isPassThrough = !userRes || userRes instanceof import_server.NextResponse && userRes.headers.get("x-middleware-next") === "1";
71
+ if (isPassThrough) {
72
+ const res2 = import_server.NextResponse.next({ request: { headers: requestHeaders } });
73
+ if (userRes instanceof import_server.NextResponse) {
74
+ userRes.cookies.getAll().forEach((c) => res2.cookies.set(c));
75
+ userRes.headers.forEach((v, k) => {
76
+ if (k !== "x-middleware-next") res2.headers.set(k, v);
77
+ });
78
+ }
79
+ return commitAnonId(res2, anon, req);
80
+ }
81
+ const res = userRes instanceof import_server.NextResponse ? userRes : userRes instanceof Response ? new import_server.NextResponse(userRes.body, userRes) : import_server.NextResponse.next({ request: { headers: requestHeaders } });
82
+ return commitAnonId(res, anon, req);
83
+ };
84
+ }
85
+ var middleware = withShipeasy();
86
+ var config = {
87
+ matcher: ["/((?!api/|_next/|.*\\..*).*)"]
88
+ };
89
+ // Annotate the CommonJS export names for ESM import in node:
90
+ 0 && (module.exports = {
91
+ ANON_ID_COOKIE,
92
+ commitAnonId,
93
+ config,
94
+ middleware,
95
+ readOrMintAnonId,
96
+ withShipeasy
97
+ });
@@ -0,0 +1,67 @@
1
+ // src/next/index.ts
2
+ import { NextResponse } from "next/server";
3
+ var ANON_ID_COOKIE = "__se_anon_id";
4
+ var ANON_ID_RX = /^[A-Za-z0-9_-]{1,64}$/;
5
+ var ANON_ID_MAX_AGE = 60 * 60 * 24 * 365;
6
+ function mint() {
7
+ return typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : `anon_${Math.random().toString(36).slice(2)}`;
8
+ }
9
+ function appendCookie(headers, name, value) {
10
+ const existing = headers.get("cookie") ?? "";
11
+ const parts = existing.split(";").map((c) => c.trim()).filter((c) => c && !c.startsWith(`${name}=`));
12
+ parts.push(`${name}=${value}`);
13
+ headers.set("cookie", parts.join("; "));
14
+ }
15
+ function readOrMintAnonId(req, requestHeaders) {
16
+ const raw = req.cookies.get(ANON_ID_COOKIE)?.value;
17
+ const valid = !!raw && ANON_ID_RX.test(raw);
18
+ const anonId = valid ? raw : mint();
19
+ const minted = !valid;
20
+ if (minted && requestHeaders) appendCookie(requestHeaders, ANON_ID_COOKIE, anonId);
21
+ return { anonId, minted };
22
+ }
23
+ function commitAnonId(res, result, req) {
24
+ if (result.minted) {
25
+ res.cookies.set(ANON_ID_COOKIE, result.anonId, {
26
+ httpOnly: false,
27
+ secure: req.nextUrl.protocol === "https:",
28
+ sameSite: "lax",
29
+ path: "/",
30
+ maxAge: ANON_ID_MAX_AGE
31
+ });
32
+ }
33
+ return res;
34
+ }
35
+ function withShipeasy(handler) {
36
+ return async (req, event) => {
37
+ const requestHeaders = new Headers(req.headers);
38
+ const anon = readOrMintAnonId(req, requestHeaders);
39
+ if (anon.minted) req.cookies.set(ANON_ID_COOKIE, anon.anonId);
40
+ const userRes = handler ? await handler(req, event) : void 0;
41
+ const isPassThrough = !userRes || userRes instanceof NextResponse && userRes.headers.get("x-middleware-next") === "1";
42
+ if (isPassThrough) {
43
+ const res2 = NextResponse.next({ request: { headers: requestHeaders } });
44
+ if (userRes instanceof NextResponse) {
45
+ userRes.cookies.getAll().forEach((c) => res2.cookies.set(c));
46
+ userRes.headers.forEach((v, k) => {
47
+ if (k !== "x-middleware-next") res2.headers.set(k, v);
48
+ });
49
+ }
50
+ return commitAnonId(res2, anon, req);
51
+ }
52
+ const res = userRes instanceof NextResponse ? userRes : userRes instanceof Response ? new NextResponse(userRes.body, userRes) : NextResponse.next({ request: { headers: requestHeaders } });
53
+ return commitAnonId(res, anon, req);
54
+ };
55
+ }
56
+ var middleware = withShipeasy();
57
+ var config = {
58
+ matcher: ["/((?!api/|_next/|.*\\..*).*)"]
59
+ };
60
+ export {
61
+ ANON_ID_COOKIE,
62
+ commitAnonId,
63
+ config,
64
+ middleware,
65
+ readOrMintAnonId,
66
+ withShipeasy
67
+ };
@@ -134,6 +134,7 @@ interface BootstrapPayload {
134
134
  experiments: Record<string, ExperimentResult<Record<string, unknown>>>;
135
135
  killswitches: Record<string, boolean | Record<string, boolean>>;
136
136
  }
137
+ declare const ANON_ID_COOKIE = "__se_anon_id";
137
138
  type FlagsClientEnv = "dev" | "staging" | "prod";
138
139
  interface FlagsClientOptions {
139
140
  apiKey: string;
@@ -291,6 +292,14 @@ interface BootstrapHtmlOptions {
291
292
  i18nProfile?: string;
292
293
  /** When true, tEl() embeds label markers so the devtools can highlight them. */
293
294
  editLabels?: boolean;
295
+ /**
296
+ * Stable anonymous bucketing id the server evaluated against. Emitted into
297
+ * window.__SE_BOOTSTRAP and persisted (pre-paint) to the first-party
298
+ * `__se_anon_id` cookie, so the browser SDK buckets identically to SSR.
299
+ * Normally minted by edge middleware; this write is the fallback for routes
300
+ * middleware doesn't cover. See experiment-platform/18-identity-bucketing.md.
301
+ */
302
+ anonId?: string;
294
303
  }
295
304
  /**
296
305
  * Returns a vanilla-JS string for a single inline <script> tag. Handles
@@ -380,4 +389,4 @@ interface SeeApi {
380
389
  */
381
390
  declare const see: SeeApi;
382
391
 
383
- export { type BootstrapHtmlOptions, type BootstrapPayload, type Consequence, type ExperimentResult, type FetchLabelsOptions, FlagsClient, type FlagsClientEnv, type FlagsClientOptions, type I18nForRequest, type LabelFile, type SeeApi, type SeeChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyServerConfig, type ShipeasyServerHandle, type User, type Violation, _resetShipeasyServerForTests, configureShipeasyServer, fetchLabelsForSSR, flags, getBootstrapHtml, getShipeasyServerClient, i18n, isExpected, see, seeContext, shipeasy, version };
392
+ export { ANON_ID_COOKIE, type BootstrapHtmlOptions, type BootstrapPayload, type Consequence, type ExperimentResult, type FetchLabelsOptions, FlagsClient, type FlagsClientEnv, type FlagsClientOptions, type I18nForRequest, type LabelFile, type SeeApi, type SeeChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyServerConfig, type ShipeasyServerHandle, type User, type Violation, _resetShipeasyServerForTests, configureShipeasyServer, fetchLabelsForSSR, flags, getBootstrapHtml, getShipeasyServerClient, i18n, isExpected, see, seeContext, shipeasy, version };
@@ -134,6 +134,7 @@ interface BootstrapPayload {
134
134
  experiments: Record<string, ExperimentResult<Record<string, unknown>>>;
135
135
  killswitches: Record<string, boolean | Record<string, boolean>>;
136
136
  }
137
+ declare const ANON_ID_COOKIE = "__se_anon_id";
137
138
  type FlagsClientEnv = "dev" | "staging" | "prod";
138
139
  interface FlagsClientOptions {
139
140
  apiKey: string;
@@ -291,6 +292,14 @@ interface BootstrapHtmlOptions {
291
292
  i18nProfile?: string;
292
293
  /** When true, tEl() embeds label markers so the devtools can highlight them. */
293
294
  editLabels?: boolean;
295
+ /**
296
+ * Stable anonymous bucketing id the server evaluated against. Emitted into
297
+ * window.__SE_BOOTSTRAP and persisted (pre-paint) to the first-party
298
+ * `__se_anon_id` cookie, so the browser SDK buckets identically to SSR.
299
+ * Normally minted by edge middleware; this write is the fallback for routes
300
+ * middleware doesn't cover. See experiment-platform/18-identity-bucketing.md.
301
+ */
302
+ anonId?: string;
294
303
  }
295
304
  /**
296
305
  * Returns a vanilla-JS string for a single inline <script> tag. Handles
@@ -380,4 +389,4 @@ interface SeeApi {
380
389
  */
381
390
  declare const see: SeeApi;
382
391
 
383
- export { type BootstrapHtmlOptions, type BootstrapPayload, type Consequence, type ExperimentResult, type FetchLabelsOptions, FlagsClient, type FlagsClientEnv, type FlagsClientOptions, type I18nForRequest, type LabelFile, type SeeApi, type SeeChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyServerConfig, type ShipeasyServerHandle, type User, type Violation, _resetShipeasyServerForTests, configureShipeasyServer, fetchLabelsForSSR, flags, getBootstrapHtml, getShipeasyServerClient, i18n, isExpected, see, seeContext, shipeasy, version };
392
+ export { ANON_ID_COOKIE, type BootstrapHtmlOptions, type BootstrapPayload, type Consequence, type ExperimentResult, type FetchLabelsOptions, FlagsClient, type FlagsClientEnv, type FlagsClientOptions, type I18nForRequest, type LabelFile, type SeeApi, type SeeChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyServerConfig, type ShipeasyServerHandle, type User, type Violation, _resetShipeasyServerForTests, configureShipeasyServer, fetchLabelsForSSR, flags, getBootstrapHtml, getShipeasyServerClient, i18n, isExpected, see, seeContext, shipeasy, version };
@@ -30,6 +30,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/server/index.ts
31
31
  var server_exports = {};
32
32
  __export(server_exports, {
33
+ ANON_ID_COOKIE: () => ANON_ID_COOKIE,
33
34
  FlagsClient: () => FlagsClient,
34
35
  _resetShipeasyServerForTests: () => _resetShipeasyServerForTests,
35
36
  configureShipeasyServer: () => configureShipeasyServer,
@@ -442,6 +443,11 @@ function matchRule(rule, user) {
442
443
  return false;
443
444
  }
444
445
  }
446
+ var ANON_ID_COOKIE = "__se_anon_id";
447
+ var ANON_ID_RX = /^[A-Za-z0-9_-]{1,64}$/;
448
+ function mintAnonId() {
449
+ return typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : `anon_${Math.random().toString(36).slice(2)}`;
450
+ }
445
451
  function evalGateInternal(gate, user) {
446
452
  if (isEnabled(gate.killswitch)) return false;
447
453
  if (!isEnabled(gate.enabled)) return false;
@@ -449,7 +455,7 @@ function evalGateInternal(gate, user) {
449
455
  if (!matchRule(rule, user)) return false;
450
456
  }
451
457
  const uid = user.user_id ?? user.anonymous_id;
452
- if (!uid) return false;
458
+ if (!uid) return gate.rolloutPct >= 1e4;
453
459
  return murmur3(`${gate.salt}:${uid}`) % 1e4 < gate.rolloutPct;
454
460
  }
455
461
  var TRUE_RX = /^(true|on|1|yes)$/i;
@@ -885,7 +891,23 @@ async function shipeasy(opts) {
885
891
  serverKey ? flags.initOnce() : Promise.resolve(),
886
892
  serverKey ? i18n.init(serverKey, profile) : Promise.resolve()
887
893
  ]);
888
- const bootstrap = flags.evaluate(opts.user ?? {}, resolvedUrlOverrides);
894
+ let anonId;
895
+ if (!opts.user?.user_id) {
896
+ if (opts.user?.anonymous_id) {
897
+ anonId = opts.user.anonymous_id;
898
+ } else {
899
+ try {
900
+ const { cookies } = await import("next/headers");
901
+ const c = await Promise.resolve(cookies());
902
+ const raw = c.get?.(ANON_ID_COOKIE)?.value;
903
+ if (raw && ANON_ID_RX.test(raw)) anonId = raw;
904
+ } catch {
905
+ }
906
+ if (!anonId) anonId = mintAnonId();
907
+ }
908
+ }
909
+ const effectiveUser = anonId ? { anonymous_id: anonId, ...opts.user } : { ...opts.user };
910
+ const bootstrap = flags.evaluate(effectiveUser, resolvedUrlOverrides);
889
911
  const i18nData = i18n.getForRequest();
890
912
  return {
891
913
  flags: bootstrap.flags,
@@ -894,7 +916,8 @@ async function shipeasy(opts) {
894
916
  getBootstrapHtml() {
895
917
  return getBootstrapHtml(bootstrap, i18nData, {
896
918
  editLabels,
897
- i18nProfile: profile
919
+ i18nProfile: profile,
920
+ anonId
898
921
  });
899
922
  }
900
923
  };
@@ -914,10 +937,16 @@ function getBootstrapHtml(bootstrap, i18nData, opts) {
914
937
  };
915
938
  if (i18nData) payload.i18n = i18nData;
916
939
  if (opts.editLabels) payload.editLabels = true;
940
+ if (opts.anonId) payload.anonId = opts.anonId;
917
941
  parts.push(
918
942
  `(function(){var Q=new URLSearchParams(location.search).has('se_edit_labels');var C=/(?:^|;\\s*)se_edit_labels=1(?:;|$)/.test(document.cookie);if(!Q&&!C)return;if(Q){try{document.cookie='se_edit_labels=1;path=/;max-age=86400;samesite=lax';}catch(_){}}var R;function P(v){if(!v||typeof v.t!=='function'||v.__sePatched)return;var O=v.t.bind(v);v.__sePatched=true;window._sei18n_t=O;v.t=function(k,vars){var r=O(k,vars);if(r===k)return k;var V='';try{if(vars&&typeof vars==='object'){var hasKey=false;for(var _k in vars){hasKey=true;break;}if(hasKey)V=JSON.stringify(vars);}}catch(_){V='';}return '\\uFFF9'+k+'\\uFFFA'+V+'\\uFFFA'+r+'\\uFFFB';};}Object.defineProperty(window,'i18n',{configurable:true,get:function(){return R;},set:function(v){P(v);R=v;}});})();`
919
943
  );
920
944
  parts.push(`window.__SE_BOOTSTRAP=${JSON.stringify(payload)};`);
945
+ if (opts.anonId) {
946
+ parts.push(
947
+ `(function(){try{var k=${JSON.stringify(ANON_ID_COOKIE)},v=${JSON.stringify(opts.anonId)};if(('; '+document.cookie).indexOf('; '+k+'=')===-1){document.cookie=k+'='+v+';path=/;max-age=31536000;samesite=lax'+(location.protocol==='https:'?';secure':'');}}catch(_){}})();`
948
+ );
949
+ }
921
950
  if (i18nData?.strings && Object.keys(i18nData.strings).length > 0) {
922
951
  parts.push(
923
952
  `(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(){};}};})();`
@@ -1003,6 +1032,7 @@ var see = Object.assign(
1003
1032
  );
1004
1033
  // Annotate the CommonJS export names for ESM import in node:
1005
1034
  0 && (module.exports = {
1035
+ ANON_ID_COOKIE,
1006
1036
  FlagsClient,
1007
1037
  _resetShipeasyServerForTests,
1008
1038
  configureShipeasyServer,
@@ -396,6 +396,11 @@ function matchRule(rule, user) {
396
396
  return false;
397
397
  }
398
398
  }
399
+ var ANON_ID_COOKIE = "__se_anon_id";
400
+ var ANON_ID_RX = /^[A-Za-z0-9_-]{1,64}$/;
401
+ function mintAnonId() {
402
+ return typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : `anon_${Math.random().toString(36).slice(2)}`;
403
+ }
399
404
  function evalGateInternal(gate, user) {
400
405
  if (isEnabled(gate.killswitch)) return false;
401
406
  if (!isEnabled(gate.enabled)) return false;
@@ -403,7 +408,7 @@ function evalGateInternal(gate, user) {
403
408
  if (!matchRule(rule, user)) return false;
404
409
  }
405
410
  const uid = user.user_id ?? user.anonymous_id;
406
- if (!uid) return false;
411
+ if (!uid) return gate.rolloutPct >= 1e4;
407
412
  return murmur3(`${gate.salt}:${uid}`) % 1e4 < gate.rolloutPct;
408
413
  }
409
414
  var TRUE_RX = /^(true|on|1|yes)$/i;
@@ -839,7 +844,23 @@ async function shipeasy(opts) {
839
844
  serverKey ? flags.initOnce() : Promise.resolve(),
840
845
  serverKey ? i18n.init(serverKey, profile) : Promise.resolve()
841
846
  ]);
842
- const bootstrap = flags.evaluate(opts.user ?? {}, resolvedUrlOverrides);
847
+ let anonId;
848
+ if (!opts.user?.user_id) {
849
+ if (opts.user?.anonymous_id) {
850
+ anonId = opts.user.anonymous_id;
851
+ } else {
852
+ try {
853
+ const { cookies } = await import("next/headers");
854
+ const c = await Promise.resolve(cookies());
855
+ const raw = c.get?.(ANON_ID_COOKIE)?.value;
856
+ if (raw && ANON_ID_RX.test(raw)) anonId = raw;
857
+ } catch {
858
+ }
859
+ if (!anonId) anonId = mintAnonId();
860
+ }
861
+ }
862
+ const effectiveUser = anonId ? { anonymous_id: anonId, ...opts.user } : { ...opts.user };
863
+ const bootstrap = flags.evaluate(effectiveUser, resolvedUrlOverrides);
843
864
  const i18nData = i18n.getForRequest();
844
865
  return {
845
866
  flags: bootstrap.flags,
@@ -848,7 +869,8 @@ async function shipeasy(opts) {
848
869
  getBootstrapHtml() {
849
870
  return getBootstrapHtml(bootstrap, i18nData, {
850
871
  editLabels,
851
- i18nProfile: profile
872
+ i18nProfile: profile,
873
+ anonId
852
874
  });
853
875
  }
854
876
  };
@@ -868,10 +890,16 @@ function getBootstrapHtml(bootstrap, i18nData, opts) {
868
890
  };
869
891
  if (i18nData) payload.i18n = i18nData;
870
892
  if (opts.editLabels) payload.editLabels = true;
893
+ if (opts.anonId) payload.anonId = opts.anonId;
871
894
  parts.push(
872
895
  `(function(){var Q=new URLSearchParams(location.search).has('se_edit_labels');var C=/(?:^|;\\s*)se_edit_labels=1(?:;|$)/.test(document.cookie);if(!Q&&!C)return;if(Q){try{document.cookie='se_edit_labels=1;path=/;max-age=86400;samesite=lax';}catch(_){}}var R;function P(v){if(!v||typeof v.t!=='function'||v.__sePatched)return;var O=v.t.bind(v);v.__sePatched=true;window._sei18n_t=O;v.t=function(k,vars){var r=O(k,vars);if(r===k)return k;var V='';try{if(vars&&typeof vars==='object'){var hasKey=false;for(var _k in vars){hasKey=true;break;}if(hasKey)V=JSON.stringify(vars);}}catch(_){V='';}return '\\uFFF9'+k+'\\uFFFA'+V+'\\uFFFA'+r+'\\uFFFB';};}Object.defineProperty(window,'i18n',{configurable:true,get:function(){return R;},set:function(v){P(v);R=v;}});})();`
873
896
  );
874
897
  parts.push(`window.__SE_BOOTSTRAP=${JSON.stringify(payload)};`);
898
+ if (opts.anonId) {
899
+ parts.push(
900
+ `(function(){try{var k=${JSON.stringify(ANON_ID_COOKIE)},v=${JSON.stringify(opts.anonId)};if(('; '+document.cookie).indexOf('; '+k+'=')===-1){document.cookie=k+'='+v+';path=/;max-age=31536000;samesite=lax'+(location.protocol==='https:'?';secure':'');}}catch(_){}})();`
901
+ );
902
+ }
875
903
  if (i18nData?.strings && Object.keys(i18nData.strings).length > 0) {
876
904
  parts.push(
877
905
  `(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(){};}};})();`
@@ -956,6 +984,7 @@ var see = Object.assign(
956
984
  }
957
985
  );
958
986
  export {
987
+ ANON_ID_COOKIE,
959
988
  FlagsClient,
960
989
  _resetShipeasyServerForTests,
961
990
  configureShipeasyServer,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipeasy/sdk",
3
- "version": "4.3.0",
3
+ "version": "4.5.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",
@@ -21,6 +21,9 @@
21
21
  ],
22
22
  "server": [
23
23
  "./dist/server/index.d.ts"
24
+ ],
25
+ "next": [
26
+ "./dist/next/index.d.ts"
24
27
  ]
25
28
  }
26
29
  },
@@ -39,11 +42,17 @@
39
42
  "types": "./dist/client/index.d.ts",
40
43
  "default": "./dist/client/index.js"
41
44
  },
45
+ "./next": {
46
+ "types": "./dist/next/index.d.ts",
47
+ "import": "./dist/next/index.mjs",
48
+ "default": "./dist/next/index.js"
49
+ },
42
50
  "./templates/*": "./templates/*.js"
43
51
  },
44
52
  "files": [
45
53
  "dist/server/",
46
54
  "dist/client/",
55
+ "dist/next/",
47
56
  "templates/",
48
57
  "LICENSE",
49
58
  "README.md"
@@ -58,9 +67,18 @@
58
67
  "dependencies": {
59
68
  "murmurhash-js": "^1.0.0"
60
69
  },
70
+ "peerDependencies": {
71
+ "next": ">=13"
72
+ },
73
+ "peerDependenciesMeta": {
74
+ "next": {
75
+ "optional": true
76
+ }
77
+ },
61
78
  "devDependencies": {
62
79
  "@types/murmurhash-js": "^1.0.6",
63
80
  "@types/node": "^20.0.0",
81
+ "next": "^16.2.3",
64
82
  "tsup": "^8.3.0",
65
83
  "typescript": "^5.7.4",
66
84
  "vitest": "^2.1.0",