@shipeasy/sdk 5.4.0 → 6.0.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.
package/README.md CHANGED
@@ -17,40 +17,78 @@ pnpm add @shipeasy/sdk
17
17
  For React projects use [`@shipeasy/sdk-react`](https://github.com/shipeasy-ai/sdk-react)
18
18
  which wraps this package with a `<ShipeasyProvider>` and hooks.
19
19
 
20
- ## Quickstart — browser
20
+ ## Quickstart — `configure()` once, then `new Client(user)`
21
+
22
+ The ergonomic front door: **configure once** with your key and an optional
23
+ `attributes` transform from *your own user object* to Shipeasy targeting
24
+ attributes, then evaluate per user with `new Client(user)`. The bound
25
+ `Client` takes **no user argument** on its methods — the user is bound at
26
+ construction.
27
+
28
+ ### Browser
21
29
 
22
30
  ```ts
23
- import { FlagsClientBrowser } from "@shipeasy/sdk/client";
31
+ import { configure, Client } from "@shipeasy/sdk/client";
24
32
 
25
- const client = new FlagsClientBrowser({
26
- sdkKey: process.env.NEXT_PUBLIC_SHIPEASY_CLIENT_KEY!,
33
+ // Once, at app startup:
34
+ configure({
35
+ clientKey: process.env.NEXT_PUBLIC_SHIPEASY_CLIENT_KEY!,
36
+ // Optional — omit when you already pass a plain attribute object.
37
+ attributes: (u: MyUser) => ({ user_id: u.id, plan: u.plan }),
27
38
  });
28
39
 
29
- await client.identify({ user_id: "user-123", plan: "pro" });
40
+ // Per visitor:
41
+ const flags = new Client(currentUser);
42
+ await flags.ready(); // optional — await the first /sdk/evaluate round-trip
30
43
 
31
- if (client.getFlag("new_checkout")) {
44
+ if (flags.getFlag("new_checkout")) {
32
45
  // ship it
33
46
  }
34
47
 
35
- const cfg = client.getConfig<{ max_uploads: number }>("upload_limits");
36
- const { params } = client.getExperiment("hero_cta", {
37
- primary_label: "Sign up",
38
- });
48
+ const cfg = flags.getConfig<{ max_uploads: number }>("upload_limits");
49
+ const { params } = flags.getExperiment("hero_cta", { primary_label: "Sign up" });
39
50
  ```
40
51
 
41
- `identify()` automatically merges browser context (`locale`, `timezone`,
42
- `path`, `referrer`, `screen_*`, `user_agent`) and a persisted
43
- `anonymous_id` into the payloadso gate rules can target by locale or
44
- holdouts can hash anonymous visitors out of the box.
52
+ The browser is single-user: `new Client(user)` runs the transform and
53
+ `identify()`s the result under the hood (merging browser context — `locale`,
54
+ `timezone`, `path`, `referrer`, `screen_*`, `user_agent`and a persisted
55
+ `anonymous_id`). `getFlag` reflects the latest identify; `await client.ready()`
56
+ when you need the first evaluation to have resolved.
45
57
 
46
- ## Quickstart — server (Node, Cloudflare Worker, Deno)
58
+ ### Server (Node, Cloudflare Worker, Deno)
47
59
 
48
60
  ```ts
49
- import { FlagsClient } from "@shipeasy/sdk/server";
61
+ import { configure, Client } from "@shipeasy/sdk/server";
50
62
 
51
- const client = new FlagsClient({ sdkKey: process.env.SHIPEASY_SERVER_KEY! });
63
+ // Once, at app boot:
64
+ configure({
65
+ apiKey: process.env.SHIPEASY_SERVER_KEY!,
66
+ attributes: (u: MyUser) => ({ user_id: u.id, plan: u.plan, country: u.geo.country }),
67
+ });
68
+
69
+ // Per request:
70
+ const flags = new Client(req.user);
71
+ if (flags.getFlag("new_checkout")) { /* ... */ }
72
+ const cfg = flags.getConfig("plan_limits");
73
+ ```
74
+
75
+ When you don't pass `attributes`, the transform is the **identity** function —
76
+ the user object you pass is used verbatim, so it should already be the
77
+ attribute bag (`{ user_id, anonymous_id, ...targeting }`).
78
+
79
+ ### Low-level: the `Engine` directly
80
+
81
+ `Client` is a cheap, user-bound handle over a single shared `Engine` (the
82
+ heavyweight class that owns the key, HTTP, the blob cache, and the poll timer —
83
+ **renamed from `FlagsClient` / `FlagsClientBrowser` in 6.0.0**). You can still
84
+ construct and drive an `Engine` yourself:
85
+
86
+ ```ts
87
+ import { Engine } from "@shipeasy/sdk/server";
52
88
 
53
- const cfg = await client.getConfig("plan_limits", { user_id: "u-1" });
89
+ const engine = new Engine({ apiKey: process.env.SHIPEASY_SERVER_KEY! });
90
+ await engine.initOnce();
91
+ const on = engine.getFlag("new_checkout", { user_id: "u-1", plan: "pro" });
54
92
  ```
55
93
 
56
94
  ## SSR bootstrap (flags on first paint)
@@ -181,9 +219,9 @@ so your tests never touch the network.
181
219
 
182
220
  ```ts
183
221
  // Server (Node / Cloudflare Worker / Deno)
184
- import { FlagsClient } from "@shipeasy/sdk/server";
222
+ import { Engine } from "@shipeasy/sdk/server";
185
223
 
186
- const client = FlagsClient.forTesting();
224
+ const client = Engine.forTesting();
187
225
 
188
226
  client.overrideFlag("new_checkout", true);
189
227
  client.overrideConfig("upload_limits", { max_uploads: 50 });
@@ -200,9 +238,9 @@ client.clearOverrides(); // reset every override back to default
200
238
 
201
239
  ```ts
202
240
  // Browser (vanilla JS — no React required)
203
- import { FlagsClientBrowser } from "@shipeasy/sdk/client";
241
+ import { Engine } from "@shipeasy/sdk/client";
204
242
 
205
- const client = FlagsClientBrowser.forTesting();
243
+ const client = Engine.forTesting();
206
244
 
207
245
  client.overrideFlag("new_checkout", true);
208
246
  client.overrideConfig("upload_limits", { max_uploads: 50 });
@@ -283,13 +321,13 @@ enabled/killed state into a boolean, so `OFF` folds into `DEFAULT` there.
283
321
 
284
322
  ## Change listeners
285
323
 
286
- The server `FlagsClient` fires registered listeners after a **background poll
324
+ The server `Engine` fires registered listeners after a **background poll
287
325
  returns new data** (HTTP 200, not 304) — handy for invalidating a cache or
288
326
  re-rendering when a flag flips. Returns an unsubscribe function. Never fires in
289
327
  test/offline mode (no polling happens):
290
328
 
291
329
  ```ts
292
- const client = new FlagsClient({ apiKey: process.env.SHIPEASY_SERVER_KEY! });
330
+ const client = new Engine({ apiKey: process.env.SHIPEASY_SERVER_KEY! });
293
331
  await client.init();
294
332
  const unsubscribe = client.onChange(() => {
295
333
  console.log("flag rules changed — re-evaluating");
@@ -298,7 +336,7 @@ const unsubscribe = client.onChange(() => {
298
336
  unsubscribe();
299
337
  ```
300
338
 
301
- The browser `FlagsClientBrowser` already exposes the equivalent `subscribe()`
339
+ The browser `Engine` already exposes the equivalent `subscribe()`
302
340
  (fires after each `identify()` / override change).
303
341
 
304
342
  ## Offline snapshot
@@ -317,11 +355,11 @@ two SDK wire bodies:
317
355
  ```
318
356
 
319
357
  ```ts
320
- import { FlagsClient } from "@shipeasy/sdk/server";
358
+ import { Engine } from "@shipeasy/sdk/server";
321
359
 
322
- const client = FlagsClient.fromFile("./snapshot.json");
360
+ const client = Engine.fromFile("./snapshot.json");
323
361
  // or, if you already hold the parsed object:
324
- const client = FlagsClient.fromSnapshot({ flags, experiments });
362
+ const client = Engine.fromSnapshot({ flags, experiments });
325
363
 
326
364
  client.getFlag("new_checkout", { user_id: "u1" });
327
365
  ```
@@ -202,8 +202,8 @@ declare function sameOrigin(rawUrl: string): boolean;
202
202
  * pass through unchanged (correlation is optional, never breaks the fetch).
203
203
  */
204
204
  declare function injectCorrelationHeader(args: Parameters<typeof fetch>, corr: string): Parameters<typeof fetch>;
205
- type FlagsClientBrowserEnv = "dev" | "staging" | "prod";
206
- interface FlagsClientBrowserOptions {
205
+ type EngineEnv = "dev" | "staging" | "prod";
206
+ interface EngineOptions {
207
207
  sdkKey: string;
208
208
  baseUrl?: string;
209
209
  autoGuardrails?: boolean;
@@ -222,7 +222,7 @@ interface FlagsClientBrowserOptions {
222
222
  */
223
223
  autoCollectAlways?: boolean;
224
224
  /** Which published env to read values from. Defaults to "prod". */
225
- env?: FlagsClientBrowserEnv;
225
+ env?: EngineEnv;
226
226
  /**
227
227
  * Per-evaluation usage telemetry. ON by default — each getFlag/getConfig/
228
228
  * getExperiment/getKillswitch call fires one fire-and-forget sendBeacon so
@@ -259,11 +259,11 @@ interface FlagsClientBrowserOptions {
259
259
  * Test mode — no network at all. identify()/init are no-ops (never call
260
260
  * /sdk/evaluate), track() is a no-op, telemetry is forced off, and the client
261
261
  * starts "ready" with an empty eval result. Prefer the
262
- * {@link FlagsClientBrowser.forTesting} factory over passing this directly.
262
+ * {@link Engine.forTesting} factory over passing this directly.
263
263
  */
264
264
  testMode?: boolean;
265
265
  }
266
- declare class FlagsClientBrowser {
266
+ declare class Engine {
267
267
  private readonly sdkKey;
268
268
  private readonly baseUrl;
269
269
  private readonly autoGuardrails;
@@ -288,7 +288,7 @@ declare class FlagsClientBrowser {
288
288
  private readonly configOverrides;
289
289
  private readonly experimentOverrides;
290
290
  private onOverrideChange;
291
- constructor(opts: FlagsClientBrowserOptions);
291
+ constructor(opts: EngineOptions);
292
292
  /**
293
293
  * Build a no-network, immediately-usable browser client for tests
294
294
  * (Statsig-style). identify() is a no-op (never calls /sdk/evaluate), track()
@@ -297,12 +297,12 @@ declare class FlagsClientBrowser {
297
297
  * key required.
298
298
  *
299
299
  * ```ts
300
- * const client = FlagsClientBrowser.forTesting();
300
+ * const client = Engine.forTesting();
301
301
  * client.overrideFlag("new_checkout", true);
302
302
  * client.getFlag("new_checkout"); // true
303
303
  * ```
304
304
  */
305
- static forTesting(opts?: Partial<FlagsClientBrowserOptions>): FlagsClientBrowser;
305
+ static forTesting(opts?: Partial<EngineOptions>): Engine;
306
306
  identify(user: User): Promise<void>;
307
307
  /**
308
308
  * Report a structured error into the errors primitive. Flushes immediately
@@ -411,7 +411,7 @@ interface AttachDevtoolsOptions {
411
411
  * each `identify()`/override change. Returns an unsubscribe function for
412
412
  * cleanup (e.g. React effect teardown).
413
413
  */
414
- declare function attachDevtools(client: FlagsClientBrowser, opts?: AttachDevtoolsOptions): () => void;
414
+ declare function attachDevtools(client: Engine, opts?: AttachDevtoolsOptions): () => void;
415
415
  /** Configure the singleton. Idempotent — re-calling with the same opts is a no-op. */
416
416
  interface ShipeasyClientConfig {
417
417
  /**
@@ -484,13 +484,13 @@ interface ShipeasyClientConfig {
484
484
  * Attribute names usable for targeting but never persisted in analytics
485
485
  * (LD/Statsig `privateAttributes`). Sent to the edge for evaluation, never
486
486
  * stored, and stripped from `flags.track(props)`. See
487
- * {@link FlagsClientBrowserOptions.privateAttributes}.
487
+ * {@link EngineOptions.privateAttributes}.
488
488
  */
489
489
  privateAttributes?: string[];
490
490
  /**
491
491
  * Sticky bucketing (doc 20 §2). ON by default — locks each enrolled unit to
492
492
  * its first-assigned variant via the `__se_sticky` cookie. Pass `false` to
493
- * opt out. See {@link FlagsClientBrowserOptions.stickyBucketing}.
493
+ * opt out. See {@link EngineOptions.stickyBucketing}.
494
494
  */
495
495
  stickyBucketing?: boolean;
496
496
  }
@@ -505,9 +505,9 @@ interface ShipeasyClientConfig {
505
505
  * A later flags.identify({ user_id }) overrides this in place; anonId stays stable.
506
506
  */
507
507
  declare function shipeasy(opts: ShipeasyClientConfig): () => void;
508
- declare function configureShipeasy(opts: FlagsClientBrowserOptions): FlagsClientBrowser;
508
+ declare function configureShipeasy(opts: EngineOptions): Engine;
509
509
  /** Returns the configured singleton, or null if configureShipeasy() hasn't run yet. */
510
- declare function getShipeasyClient(): FlagsClientBrowser | null;
510
+ declare function getShipeasyClient(): Engine | null;
511
511
  /**
512
512
  * Test helper — drop the singleton so the next configureShipeasy() builds fresh.
513
513
  * Not part of the documented surface; production code should never call this.
@@ -539,7 +539,7 @@ interface BootstrapPayload {
539
539
  * importing this in a module that loads before app boot is harmless.
540
540
  */
541
541
  declare const flags: {
542
- configure(opts: FlagsClientBrowserOptions): void;
542
+ configure(opts: EngineOptions): void;
543
543
  identify(user: User): Promise<void>;
544
544
  /**
545
545
  * Read a feature gate.
@@ -554,7 +554,7 @@ declare const flags: {
554
554
  getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
555
555
  getExperiment<P extends Record<string, unknown>>(name: string, defaultParams: P, decodeOrOpts?: ((raw: unknown) => P) | GetExperimentOptions<P>, variants?: Record<string, Partial<P>>): ExperimentResult<P>;
556
556
  /** Manually log an exposure for an enrolled experiment. See
557
- * {@link FlagsClientBrowser.logExposure}. No-op before configure(). */
557
+ * {@link Engine.logExposure}. No-op before configure(). */
558
558
  logExposure(name: string): void;
559
559
  track(eventName: string, props?: Record<string, unknown>): void;
560
560
  /**
@@ -583,6 +583,78 @@ declare const flags: {
583
583
  /** True once identify() has completed and flags are available. */
584
584
  readonly ready: boolean;
585
585
  };
586
+ /** Transform YOUR application's user object into Shipeasy targeting attributes. */
587
+ type AttributesFn<U = unknown> = (user: U) => User;
588
+ interface ConfigureOptions<U = unknown> extends Omit<ShipeasyClientConfig, "clientKey"> {
589
+ /** Public client key — the single key the browser side accepts (NEXT_PUBLIC_SHIPEASY_CLIENT_KEY). */
590
+ clientKey: string;
591
+ /**
592
+ * Map your own user object into the attribute bag every flag/experiment
593
+ * evaluation sees. Runs once per `new Client(user)`. Omit when you already
594
+ * pass a plain attribute object (identity transform — the object is used
595
+ * verbatim, so it should carry `user_id` + any targeting attrs).
596
+ */
597
+ attributes?: AttributesFn<U>;
598
+ }
599
+ /**
600
+ * Configure the SDK once at app boot, then evaluate per user with
601
+ * `new Client(user)`. Builds the process-wide {@link Engine} (the
602
+ * /sdk/evaluate-backed browser client) and registers the `attributes`
603
+ * transform. The first call wins; later calls reuse the existing engine
604
+ * (mirrors {@link shipeasy}).
605
+ *
606
+ * ```ts
607
+ * import { configure, Client } from "@shipeasy/sdk/client";
608
+ *
609
+ * configure({
610
+ * clientKey: process.env.NEXT_PUBLIC_SHIPEASY_CLIENT_KEY!,
611
+ * attributes: (u: MyUser) => ({ user_id: u.id, plan: u.plan }),
612
+ * });
613
+ *
614
+ * const flags = new Client(currentUser);
615
+ * await flags.ready();
616
+ * if (flags.getFlag("new_checkout")) { ... }
617
+ * ```
618
+ *
619
+ * Returns a cleanup function (same as {@link shipeasy}) that removes the
620
+ * devtools listeners — call it on teardown.
621
+ */
622
+ declare function configure<U = unknown>(opts: ConfigureOptions<U>): () => void;
623
+ /** Test seam: reset the registered attribute transform. */
624
+ declare function _resetConfigureForTests(): void;
625
+ /**
626
+ * A user-bound evaluation handle for the browser. Construct one for the current
627
+ * visitor — it's cheap (it delegates to the single {@link Engine} built by
628
+ * {@link configure}); it does NOT open its own connection. The configured
629
+ * `attributes` transform runs once here and the result is `identify()`-ed into
630
+ * the engine (fire-and-forget, since identify is async).
631
+ *
632
+ * Because the browser is single-user, all bound handles share the engine's
633
+ * latest eval result. Use {@link Client.ready} to await the first evaluation.
634
+ *
635
+ * ```ts
636
+ * const flags = new Client(currentUser);
637
+ * await flags.ready();
638
+ * flags.getFlag("new_checkout"); // no user arg — bound at construction
639
+ * flags.getExperiment("price_test", { price: 9 });
640
+ * ```
641
+ */
642
+ declare class Client<U = unknown> {
643
+ private readonly engine;
644
+ /** The resolved attribute bag this handle evaluates against. */
645
+ readonly attributes: User;
646
+ private readonly _identify;
647
+ constructor(user: U);
648
+ /** Resolves once the engine's identify() for this user has completed. */
649
+ ready(): Promise<void>;
650
+ getFlag(name: string, defaultValue?: boolean): boolean;
651
+ getFlagDetail(name: string): FlagDetail;
652
+ getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
653
+ getConfig<T = unknown>(name: string, opts: GetConfigOptions<T>): T;
654
+ getExperiment<P extends Record<string, unknown>>(name: string, defaultParams: P, decode?: (raw: unknown) => P): ExperimentResult<P>;
655
+ /** Read a killswitch (not user-bound; mirrors {@link Engine.getKillswitch}). */
656
+ getKillswitch(name: string, switchKey?: string): boolean;
657
+ }
586
658
  interface SeeApi {
587
659
  /**
588
660
  * Report a handled problem and its product consequence:
@@ -674,4 +746,4 @@ interface I18nFacade {
674
746
  }
675
747
  declare const i18n: I18nFacade;
676
748
 
677
- export { type AutoCollectGroups, type BootstrapPayload, type Consequence, type ExperimentResult, FLAG_REASONS, type FlagDetail, type FlagReason, FlagsClientBrowser, type FlagsClientBrowserEnv, type FlagsClientBrowserOptions, type GetConfigOptions, type GetExperimentOptions, type I18nFacade, type I18nKey, type I18nRichComponents, type I18nString, type I18nTagRenderer, type I18nVariables, LABEL_MARKER_END, LABEL_MARKER_RE, LABEL_MARKER_SEP, LABEL_MARKER_START, type LabelAttrs, type SeeApi, type SeeChain, type SeeControlFlowChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyClientConfig, type ShipeasySdkBridge, type User, type Violation, _resetShipeasyForTests, attachDevtools, configureShipeasy, encodeLabelMarker, flags, getShipeasyClient, i18n, injectCorrelationHeader, isDevtoolsRequested, labelAttrs, loadDevtools, readConfigOverride, readExpOverride, readGateOverride, sameOrigin, see, shipeasy, version };
749
+ export { type AttributesFn, type AutoCollectGroups, type BootstrapPayload, Client, type ConfigureOptions, type Consequence, Engine, type EngineEnv, type EngineOptions, type ExperimentResult, FLAG_REASONS, type FlagDetail, type FlagReason, type GetConfigOptions, type GetExperimentOptions, type I18nFacade, type I18nKey, type I18nRichComponents, type I18nString, type I18nTagRenderer, type I18nVariables, LABEL_MARKER_END, LABEL_MARKER_RE, LABEL_MARKER_SEP, LABEL_MARKER_START, type LabelAttrs, type SeeApi, type SeeChain, type SeeControlFlowChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyClientConfig, type ShipeasySdkBridge, type User, type Violation, _resetConfigureForTests, _resetShipeasyForTests, attachDevtools, configure, configureShipeasy, encodeLabelMarker, flags, getShipeasyClient, i18n, injectCorrelationHeader, isDevtoolsRequested, labelAttrs, loadDevtools, readConfigOverride, readExpOverride, readGateOverride, sameOrigin, see, shipeasy, version };
@@ -202,8 +202,8 @@ declare function sameOrigin(rawUrl: string): boolean;
202
202
  * pass through unchanged (correlation is optional, never breaks the fetch).
203
203
  */
204
204
  declare function injectCorrelationHeader(args: Parameters<typeof fetch>, corr: string): Parameters<typeof fetch>;
205
- type FlagsClientBrowserEnv = "dev" | "staging" | "prod";
206
- interface FlagsClientBrowserOptions {
205
+ type EngineEnv = "dev" | "staging" | "prod";
206
+ interface EngineOptions {
207
207
  sdkKey: string;
208
208
  baseUrl?: string;
209
209
  autoGuardrails?: boolean;
@@ -222,7 +222,7 @@ interface FlagsClientBrowserOptions {
222
222
  */
223
223
  autoCollectAlways?: boolean;
224
224
  /** Which published env to read values from. Defaults to "prod". */
225
- env?: FlagsClientBrowserEnv;
225
+ env?: EngineEnv;
226
226
  /**
227
227
  * Per-evaluation usage telemetry. ON by default — each getFlag/getConfig/
228
228
  * getExperiment/getKillswitch call fires one fire-and-forget sendBeacon so
@@ -259,11 +259,11 @@ interface FlagsClientBrowserOptions {
259
259
  * Test mode — no network at all. identify()/init are no-ops (never call
260
260
  * /sdk/evaluate), track() is a no-op, telemetry is forced off, and the client
261
261
  * starts "ready" with an empty eval result. Prefer the
262
- * {@link FlagsClientBrowser.forTesting} factory over passing this directly.
262
+ * {@link Engine.forTesting} factory over passing this directly.
263
263
  */
264
264
  testMode?: boolean;
265
265
  }
266
- declare class FlagsClientBrowser {
266
+ declare class Engine {
267
267
  private readonly sdkKey;
268
268
  private readonly baseUrl;
269
269
  private readonly autoGuardrails;
@@ -288,7 +288,7 @@ declare class FlagsClientBrowser {
288
288
  private readonly configOverrides;
289
289
  private readonly experimentOverrides;
290
290
  private onOverrideChange;
291
- constructor(opts: FlagsClientBrowserOptions);
291
+ constructor(opts: EngineOptions);
292
292
  /**
293
293
  * Build a no-network, immediately-usable browser client for tests
294
294
  * (Statsig-style). identify() is a no-op (never calls /sdk/evaluate), track()
@@ -297,12 +297,12 @@ declare class FlagsClientBrowser {
297
297
  * key required.
298
298
  *
299
299
  * ```ts
300
- * const client = FlagsClientBrowser.forTesting();
300
+ * const client = Engine.forTesting();
301
301
  * client.overrideFlag("new_checkout", true);
302
302
  * client.getFlag("new_checkout"); // true
303
303
  * ```
304
304
  */
305
- static forTesting(opts?: Partial<FlagsClientBrowserOptions>): FlagsClientBrowser;
305
+ static forTesting(opts?: Partial<EngineOptions>): Engine;
306
306
  identify(user: User): Promise<void>;
307
307
  /**
308
308
  * Report a structured error into the errors primitive. Flushes immediately
@@ -411,7 +411,7 @@ interface AttachDevtoolsOptions {
411
411
  * each `identify()`/override change. Returns an unsubscribe function for
412
412
  * cleanup (e.g. React effect teardown).
413
413
  */
414
- declare function attachDevtools(client: FlagsClientBrowser, opts?: AttachDevtoolsOptions): () => void;
414
+ declare function attachDevtools(client: Engine, opts?: AttachDevtoolsOptions): () => void;
415
415
  /** Configure the singleton. Idempotent — re-calling with the same opts is a no-op. */
416
416
  interface ShipeasyClientConfig {
417
417
  /**
@@ -484,13 +484,13 @@ interface ShipeasyClientConfig {
484
484
  * Attribute names usable for targeting but never persisted in analytics
485
485
  * (LD/Statsig `privateAttributes`). Sent to the edge for evaluation, never
486
486
  * stored, and stripped from `flags.track(props)`. See
487
- * {@link FlagsClientBrowserOptions.privateAttributes}.
487
+ * {@link EngineOptions.privateAttributes}.
488
488
  */
489
489
  privateAttributes?: string[];
490
490
  /**
491
491
  * Sticky bucketing (doc 20 §2). ON by default — locks each enrolled unit to
492
492
  * its first-assigned variant via the `__se_sticky` cookie. Pass `false` to
493
- * opt out. See {@link FlagsClientBrowserOptions.stickyBucketing}.
493
+ * opt out. See {@link EngineOptions.stickyBucketing}.
494
494
  */
495
495
  stickyBucketing?: boolean;
496
496
  }
@@ -505,9 +505,9 @@ interface ShipeasyClientConfig {
505
505
  * A later flags.identify({ user_id }) overrides this in place; anonId stays stable.
506
506
  */
507
507
  declare function shipeasy(opts: ShipeasyClientConfig): () => void;
508
- declare function configureShipeasy(opts: FlagsClientBrowserOptions): FlagsClientBrowser;
508
+ declare function configureShipeasy(opts: EngineOptions): Engine;
509
509
  /** Returns the configured singleton, or null if configureShipeasy() hasn't run yet. */
510
- declare function getShipeasyClient(): FlagsClientBrowser | null;
510
+ declare function getShipeasyClient(): Engine | null;
511
511
  /**
512
512
  * Test helper — drop the singleton so the next configureShipeasy() builds fresh.
513
513
  * Not part of the documented surface; production code should never call this.
@@ -539,7 +539,7 @@ interface BootstrapPayload {
539
539
  * importing this in a module that loads before app boot is harmless.
540
540
  */
541
541
  declare const flags: {
542
- configure(opts: FlagsClientBrowserOptions): void;
542
+ configure(opts: EngineOptions): void;
543
543
  identify(user: User): Promise<void>;
544
544
  /**
545
545
  * Read a feature gate.
@@ -554,7 +554,7 @@ declare const flags: {
554
554
  getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
555
555
  getExperiment<P extends Record<string, unknown>>(name: string, defaultParams: P, decodeOrOpts?: ((raw: unknown) => P) | GetExperimentOptions<P>, variants?: Record<string, Partial<P>>): ExperimentResult<P>;
556
556
  /** Manually log an exposure for an enrolled experiment. See
557
- * {@link FlagsClientBrowser.logExposure}. No-op before configure(). */
557
+ * {@link Engine.logExposure}. No-op before configure(). */
558
558
  logExposure(name: string): void;
559
559
  track(eventName: string, props?: Record<string, unknown>): void;
560
560
  /**
@@ -583,6 +583,78 @@ declare const flags: {
583
583
  /** True once identify() has completed and flags are available. */
584
584
  readonly ready: boolean;
585
585
  };
586
+ /** Transform YOUR application's user object into Shipeasy targeting attributes. */
587
+ type AttributesFn<U = unknown> = (user: U) => User;
588
+ interface ConfigureOptions<U = unknown> extends Omit<ShipeasyClientConfig, "clientKey"> {
589
+ /** Public client key — the single key the browser side accepts (NEXT_PUBLIC_SHIPEASY_CLIENT_KEY). */
590
+ clientKey: string;
591
+ /**
592
+ * Map your own user object into the attribute bag every flag/experiment
593
+ * evaluation sees. Runs once per `new Client(user)`. Omit when you already
594
+ * pass a plain attribute object (identity transform — the object is used
595
+ * verbatim, so it should carry `user_id` + any targeting attrs).
596
+ */
597
+ attributes?: AttributesFn<U>;
598
+ }
599
+ /**
600
+ * Configure the SDK once at app boot, then evaluate per user with
601
+ * `new Client(user)`. Builds the process-wide {@link Engine} (the
602
+ * /sdk/evaluate-backed browser client) and registers the `attributes`
603
+ * transform. The first call wins; later calls reuse the existing engine
604
+ * (mirrors {@link shipeasy}).
605
+ *
606
+ * ```ts
607
+ * import { configure, Client } from "@shipeasy/sdk/client";
608
+ *
609
+ * configure({
610
+ * clientKey: process.env.NEXT_PUBLIC_SHIPEASY_CLIENT_KEY!,
611
+ * attributes: (u: MyUser) => ({ user_id: u.id, plan: u.plan }),
612
+ * });
613
+ *
614
+ * const flags = new Client(currentUser);
615
+ * await flags.ready();
616
+ * if (flags.getFlag("new_checkout")) { ... }
617
+ * ```
618
+ *
619
+ * Returns a cleanup function (same as {@link shipeasy}) that removes the
620
+ * devtools listeners — call it on teardown.
621
+ */
622
+ declare function configure<U = unknown>(opts: ConfigureOptions<U>): () => void;
623
+ /** Test seam: reset the registered attribute transform. */
624
+ declare function _resetConfigureForTests(): void;
625
+ /**
626
+ * A user-bound evaluation handle for the browser. Construct one for the current
627
+ * visitor — it's cheap (it delegates to the single {@link Engine} built by
628
+ * {@link configure}); it does NOT open its own connection. The configured
629
+ * `attributes` transform runs once here and the result is `identify()`-ed into
630
+ * the engine (fire-and-forget, since identify is async).
631
+ *
632
+ * Because the browser is single-user, all bound handles share the engine's
633
+ * latest eval result. Use {@link Client.ready} to await the first evaluation.
634
+ *
635
+ * ```ts
636
+ * const flags = new Client(currentUser);
637
+ * await flags.ready();
638
+ * flags.getFlag("new_checkout"); // no user arg — bound at construction
639
+ * flags.getExperiment("price_test", { price: 9 });
640
+ * ```
641
+ */
642
+ declare class Client<U = unknown> {
643
+ private readonly engine;
644
+ /** The resolved attribute bag this handle evaluates against. */
645
+ readonly attributes: User;
646
+ private readonly _identify;
647
+ constructor(user: U);
648
+ /** Resolves once the engine's identify() for this user has completed. */
649
+ ready(): Promise<void>;
650
+ getFlag(name: string, defaultValue?: boolean): boolean;
651
+ getFlagDetail(name: string): FlagDetail;
652
+ getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
653
+ getConfig<T = unknown>(name: string, opts: GetConfigOptions<T>): T;
654
+ getExperiment<P extends Record<string, unknown>>(name: string, defaultParams: P, decode?: (raw: unknown) => P): ExperimentResult<P>;
655
+ /** Read a killswitch (not user-bound; mirrors {@link Engine.getKillswitch}). */
656
+ getKillswitch(name: string, switchKey?: string): boolean;
657
+ }
586
658
  interface SeeApi {
587
659
  /**
588
660
  * Report a handled problem and its product consequence:
@@ -674,4 +746,4 @@ interface I18nFacade {
674
746
  }
675
747
  declare const i18n: I18nFacade;
676
748
 
677
- export { type AutoCollectGroups, type BootstrapPayload, type Consequence, type ExperimentResult, FLAG_REASONS, type FlagDetail, type FlagReason, FlagsClientBrowser, type FlagsClientBrowserEnv, type FlagsClientBrowserOptions, type GetConfigOptions, type GetExperimentOptions, type I18nFacade, type I18nKey, type I18nRichComponents, type I18nString, type I18nTagRenderer, type I18nVariables, LABEL_MARKER_END, LABEL_MARKER_RE, LABEL_MARKER_SEP, LABEL_MARKER_START, type LabelAttrs, type SeeApi, type SeeChain, type SeeControlFlowChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyClientConfig, type ShipeasySdkBridge, type User, type Violation, _resetShipeasyForTests, attachDevtools, configureShipeasy, encodeLabelMarker, flags, getShipeasyClient, i18n, injectCorrelationHeader, isDevtoolsRequested, labelAttrs, loadDevtools, readConfigOverride, readExpOverride, readGateOverride, sameOrigin, see, shipeasy, version };
749
+ export { type AttributesFn, type AutoCollectGroups, type BootstrapPayload, Client, type ConfigureOptions, type Consequence, Engine, type EngineEnv, type EngineOptions, type ExperimentResult, FLAG_REASONS, type FlagDetail, type FlagReason, type GetConfigOptions, type GetExperimentOptions, type I18nFacade, type I18nKey, type I18nRichComponents, type I18nString, type I18nTagRenderer, type I18nVariables, LABEL_MARKER_END, LABEL_MARKER_RE, LABEL_MARKER_SEP, LABEL_MARKER_START, type LabelAttrs, type SeeApi, type SeeChain, type SeeControlFlowChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyClientConfig, type ShipeasySdkBridge, type User, type Violation, _resetConfigureForTests, _resetShipeasyForTests, attachDevtools, configure, configureShipeasy, encodeLabelMarker, flags, getShipeasyClient, i18n, injectCorrelationHeader, isDevtoolsRequested, labelAttrs, loadDevtools, readConfigOverride, readExpOverride, readGateOverride, sameOrigin, see, shipeasy, version };