@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.
@@ -101,8 +101,8 @@ interface AutoCollectGroups {
101
101
  errors: boolean;
102
102
  engagement: boolean;
103
103
  }
104
- type FlagsClientBrowserEnv = "dev" | "staging" | "prod";
105
- interface FlagsClientBrowserOptions {
104
+ type EngineEnv = "dev" | "staging" | "prod";
105
+ interface EngineOptions {
106
106
  sdkKey: string;
107
107
  baseUrl?: string;
108
108
  autoGuardrails?: boolean;
@@ -121,7 +121,7 @@ interface FlagsClientBrowserOptions {
121
121
  */
122
122
  autoCollectAlways?: boolean;
123
123
  /** Which published env to read values from. Defaults to "prod". */
124
- env?: FlagsClientBrowserEnv;
124
+ env?: EngineEnv;
125
125
  /**
126
126
  * Per-evaluation usage telemetry. ON by default — each getFlag/getConfig/
127
127
  * getExperiment/getKillswitch call fires one fire-and-forget sendBeacon so
@@ -158,11 +158,11 @@ interface FlagsClientBrowserOptions {
158
158
  * Test mode — no network at all. identify()/init are no-ops (never call
159
159
  * /sdk/evaluate), track() is a no-op, telemetry is forced off, and the client
160
160
  * starts "ready" with an empty eval result. Prefer the
161
- * {@link FlagsClientBrowser.forTesting} factory over passing this directly.
161
+ * {@link Engine.forTesting} factory over passing this directly.
162
162
  */
163
163
  testMode?: boolean;
164
164
  }
165
- declare class FlagsClientBrowser {
165
+ declare class Engine {
166
166
  private readonly sdkKey;
167
167
  private readonly baseUrl;
168
168
  private readonly autoGuardrails;
@@ -187,7 +187,7 @@ declare class FlagsClientBrowser {
187
187
  private readonly configOverrides;
188
188
  private readonly experimentOverrides;
189
189
  private onOverrideChange;
190
- constructor(opts: FlagsClientBrowserOptions);
190
+ constructor(opts: EngineOptions);
191
191
  /**
192
192
  * Build a no-network, immediately-usable browser client for tests
193
193
  * (Statsig-style). identify() is a no-op (never calls /sdk/evaluate), track()
@@ -196,12 +196,12 @@ declare class FlagsClientBrowser {
196
196
  * key required.
197
197
  *
198
198
  * ```ts
199
- * const client = FlagsClientBrowser.forTesting();
199
+ * const client = Engine.forTesting();
200
200
  * client.overrideFlag("new_checkout", true);
201
201
  * client.getFlag("new_checkout"); // true
202
202
  * ```
203
203
  */
204
- static forTesting(opts?: Partial<FlagsClientBrowserOptions>): FlagsClientBrowser;
204
+ static forTesting(opts?: Partial<EngineOptions>): Engine;
205
205
  identify(user: User): Promise<void>;
206
206
  /**
207
207
  * Report a structured error into the errors primitive. Flushes immediately
@@ -289,10 +289,10 @@ interface ShipeasySdkBridge {
289
289
  *
290
290
  * ```ts
291
291
  * import { OpenFeature } from "@openfeature/web-sdk";
292
- * import { FlagsClientBrowser } from "@shipeasy/sdk/client";
292
+ * import { Engine } from "@shipeasy/sdk/client";
293
293
  * import { ShipeasyProvider } from "@shipeasy/sdk/openfeature-web";
294
294
  *
295
- * const client = new FlagsClientBrowser({ sdkKey: NEXT_PUBLIC_SHIPEASY_CLIENT_KEY });
295
+ * const client = new Engine({ sdkKey: NEXT_PUBLIC_SHIPEASY_CLIENT_KEY });
296
296
  * await OpenFeature.setContext({ targetingKey: "u1", plan: "pro" });
297
297
  * await OpenFeature.setProviderAndWait(new ShipeasyProvider(client));
298
298
  *
@@ -307,7 +307,7 @@ interface ShipeasySdkBridge {
307
307
 
308
308
  /**
309
309
  * Shipeasy OpenFeature provider (client/web paradigm). Wraps a
310
- * `FlagsClientBrowser`. Context changes are reconciled through `identify()`;
310
+ * `Engine`. Context changes are reconciled through `identify()`;
311
311
  * flag reads come from the cached eval result.
312
312
  */
313
313
  declare class ShipeasyProvider implements Provider {
@@ -316,7 +316,7 @@ declare class ShipeasyProvider implements Provider {
316
316
  readonly name: "shipeasy";
317
317
  };
318
318
  readonly runsOn: "client";
319
- constructor(client: FlagsClientBrowser);
319
+ constructor(client: Engine);
320
320
  /** Identify with the initial static context so the first read is warm. */
321
321
  initialize(context?: EvaluationContext): Promise<void>;
322
322
  /** Re-identify when the application author changes the static context. */
@@ -181,7 +181,7 @@ interface StickyEntry {
181
181
  /**
182
182
  * Pluggable sticky-bucketing store for the server (doc 20 §2). Keyed by the
183
183
  * bucketing unit; the value is that unit's per-experiment assignments. Absent
184
- * from {@link FlagsClientOptions} ⇒ today's deterministic behaviour. Use
184
+ * from {@link EngineOptions} ⇒ today's deterministic behaviour. Use
185
185
  * {@link createInMemoryStickyStore} or a cookie-bridge built from request
186
186
  * cookies.
187
187
  */
@@ -198,7 +198,7 @@ interface Killswitch {
198
198
  killed: 0 | 1 | boolean;
199
199
  switches?: Record<string, 0 | 1 | boolean>;
200
200
  }
201
- /** Body of `GET /sdk/flags` — the snapshot's `flags` field. See {@link FlagsClient.fromSnapshot}. */
201
+ /** Body of `GET /sdk/flags` — the snapshot's `flags` field. See {@link Engine.fromSnapshot}. */
202
202
  interface FlagsBlob {
203
203
  version: string;
204
204
  plan: string;
@@ -208,7 +208,7 @@ interface FlagsBlob {
208
208
  }>;
209
209
  killswitches: Record<string, Killswitch>;
210
210
  }
211
- /** Body of `GET /sdk/experiments` — the snapshot's `experiments` field. See {@link FlagsClient.fromSnapshot}. */
211
+ /** Body of `GET /sdk/experiments` — the snapshot's `experiments` field. See {@link Engine.fromSnapshot}. */
212
212
  interface ExpsBlob {
213
213
  version: string;
214
214
  universes: Record<string, Universe>;
@@ -221,12 +221,12 @@ interface BootstrapPayload {
221
221
  killswitches: Record<string, boolean | Record<string, boolean>>;
222
222
  }
223
223
  declare const ANON_ID_COOKIE = "__se_anon_id";
224
- type FlagsClientEnv = "dev" | "staging" | "prod";
225
- interface FlagsClientOptions {
224
+ type EngineEnv = "dev" | "staging" | "prod";
225
+ interface EngineOptions {
226
226
  apiKey: string;
227
227
  baseUrl?: string;
228
228
  /** Which published env to read values from. Defaults to "prod". */
229
- env?: FlagsClientEnv;
229
+ env?: EngineEnv;
230
230
  /**
231
231
  * Preload the flags blob synchronously without a network fetch. Primarily
232
232
  * for tests; production callers should rely on init()/initOnce().
@@ -261,12 +261,12 @@ interface FlagsClientOptions {
261
261
  /**
262
262
  * Test mode — no network at all. init()/initOnce() are no-ops (never fetch),
263
263
  * track() is a no-op, telemetry is forced off, and the client starts
264
- * "initialized" with an empty blob. Prefer the {@link FlagsClient.forTesting}
264
+ * "initialized" with an empty blob. Prefer the {@link Engine.forTesting}
265
265
  * factory over passing this directly.
266
266
  */
267
267
  testMode?: boolean;
268
268
  }
269
- declare class FlagsClient {
269
+ declare class Engine {
270
270
  private readonly apiKey;
271
271
  private readonly baseUrl;
272
272
  private readonly env;
@@ -286,7 +286,7 @@ declare class FlagsClient {
286
286
  private readonly configOverrides;
287
287
  private readonly experimentOverrides;
288
288
  private readonly changeListeners;
289
- constructor(opts: FlagsClientOptions);
289
+ constructor(opts: EngineOptions);
290
290
  /**
291
291
  * Build a no-network, immediately-usable client for tests (Statsig-style).
292
292
  * init()/initOnce() are no-ops (never fetch), track() is a no-op, telemetry
@@ -294,12 +294,12 @@ declare class FlagsClient {
294
294
  * with overrideFlag/overrideConfig/overrideExperiment. No SDK key required.
295
295
  *
296
296
  * ```ts
297
- * const client = FlagsClient.forTesting();
297
+ * const client = Engine.forTesting();
298
298
  * client.overrideFlag("new_checkout", true);
299
299
  * client.getFlag("new_checkout", { user_id: "u1" }); // true
300
300
  * ```
301
301
  */
302
- static forTesting(opts?: Partial<FlagsClientOptions>): FlagsClient;
302
+ static forTesting(opts?: Partial<EngineOptions>): Engine;
303
303
  /**
304
304
  * Build a fully OFFLINE client from a pre-captured snapshot — no network ever.
305
305
  * Reuses the test-mode plumbing (init()/initOnce()/track() are no-ops,
@@ -313,14 +313,14 @@ declare class FlagsClient {
313
313
  static fromSnapshot(snapshot: {
314
314
  flags: FlagsBlob;
315
315
  experiments: ExpsBlob;
316
- }): FlagsClient;
316
+ }): Engine;
317
317
  /**
318
318
  * Build a fully OFFLINE client from a snapshot JSON file on disk (Node only —
319
319
  * not available in the browser entrypoint). The file must contain
320
320
  * `{ "flags": <GET /sdk/flags body>, "experiments": <GET /sdk/experiments body> }`.
321
- * See {@link FlagsClient.fromSnapshot}.
321
+ * See {@link Engine.fromSnapshot}.
322
322
  */
323
- static fromFile(path: string): FlagsClient;
323
+ static fromFile(path: string): Engine;
324
324
  init(): Promise<void>;
325
325
  initOnce(): Promise<void>;
326
326
  /** Force `getFlag(name, …)` to return `value`, ignoring the fetched gate. */
@@ -440,8 +440,8 @@ interface FetchLabelsOptions {
440
440
  timeoutMs?: number;
441
441
  }
442
442
  declare function fetchLabelsForSSR(opts: FetchLabelsOptions): Promise<LabelFile | null>;
443
- declare function configureShipeasyServer(opts: FlagsClientOptions): FlagsClient;
444
- declare function getShipeasyServerClient(): FlagsClient | null;
443
+ declare function configureShipeasyServer(opts: EngineOptions): Engine;
444
+ declare function getShipeasyServerClient(): Engine | null;
445
445
  declare function _resetShipeasyServerForTests(): void;
446
446
  interface ShipeasyServerConfig {
447
447
  /**
@@ -463,13 +463,13 @@ interface ShipeasyServerConfig {
463
463
  /**
464
464
  * Disable per-evaluation usage telemetry. ON by default. On Cloudflare
465
465
  * Workers each beacon is an outbound subrequest, so disable on hot SSR paths
466
- * that evaluate many flags per request. See {@link FlagsClientOptions.disableTelemetry}.
466
+ * that evaluate many flags per request. See {@link EngineOptions.disableTelemetry}.
467
467
  */
468
468
  disableTelemetry?: boolean;
469
469
  /**
470
470
  * Attribute names usable for targeting but never persisted in analytics
471
471
  * (LD/Statsig `privateAttributes`). Stripped from every outbound `track()`
472
- * payload. See {@link FlagsClientOptions.privateAttributes}.
472
+ * payload. See {@link EngineOptions.privateAttributes}.
473
473
  */
474
474
  privateAttributes?: string[];
475
475
  }
@@ -547,7 +547,7 @@ declare function getBootstrapData(bootstrap: BootstrapPayload | null, i18nData:
547
547
  */
548
548
  declare function getBootstrapTags(bootstrap: BootstrapPayload | null, i18nData: I18nForRequest | null, opts: BootstrapEmitOptions): string;
549
549
  declare const flags: {
550
- configure(opts: FlagsClientOptions): void;
550
+ configure(opts: EngineOptions): void;
551
551
  /**
552
552
  * Long-running server: starts the background poll. Call once at app boot.
553
553
  * Throws if the initial fetch fails (caller decides whether to crash or degrade).
@@ -570,7 +570,7 @@ declare const flags: {
570
570
  ks(name: string, switchKey?: string): boolean;
571
571
  track(userId: string, eventName: string, props?: Record<string, unknown>): void;
572
572
  /** Emit an exposure for an enrolled experiment at the decision point. See
573
- * {@link FlagsClient.logExposure}. No-op before configure(). */
573
+ * {@link Engine.logExposure}. No-op before configure(). */
574
574
  logExposure(user: string | User, name: string): void;
575
575
  /**
576
576
  * Evaluate all flags / configs / experiments for a user against the locally
@@ -579,6 +579,65 @@ declare const flags: {
579
579
  */
580
580
  evaluate(user: User, rawUrl?: string): BootstrapPayload;
581
581
  };
582
+ /** Transform YOUR application's user object into Shipeasy targeting attributes. */
583
+ type AttributesFn<U = unknown> = (user: U) => User;
584
+ interface ConfigureOptions<U = unknown> extends Omit<EngineOptions, "apiKey"> {
585
+ /** Server key — the single key the server side accepts (SHIPEASY_SERVER_KEY). */
586
+ apiKey: string;
587
+ /**
588
+ * Map your own user object into the attribute bag every flag/experiment
589
+ * evaluation sees. Runs once per `new Client(user)`. Omit when you already
590
+ * pass a plain attribute object (identity transform — the object is used
591
+ * verbatim, so it should carry `user_id`/`anonymous_id` + any targeting attrs).
592
+ */
593
+ attributes?: AttributesFn<U>;
594
+ }
595
+ /**
596
+ * Configure the SDK once at app boot, then evaluate per user with
597
+ * `new Client(user)`. Builds the process-wide {@link Engine} (polling + blob
598
+ * cache + HTTP) and registers the `attributes` transform. The first call wins;
599
+ * later calls return the existing engine (mirrors {@link configureShipeasyServer}).
600
+ *
601
+ * ```ts
602
+ * import { configure, Client } from "@shipeasy/sdk/server";
603
+ *
604
+ * configure({
605
+ * apiKey: process.env.SHIPEASY_SERVER_KEY!,
606
+ * attributes: (u: MyUser) => ({ user_id: u.id, plan: u.plan, country: u.geo.country }),
607
+ * });
608
+ *
609
+ * const flags = new Client(currentUser);
610
+ * if (flags.getFlag("new_checkout")) { ... }
611
+ * ```
612
+ */
613
+ declare function configure<U = unknown>(opts: ConfigureOptions<U>): Engine;
614
+ /** Test seam: reset the registered attribute transform. */
615
+ declare function _resetConfigureForTests(): void;
616
+ /**
617
+ * A user-bound evaluation handle. Construct one per user/request — it's cheap
618
+ * (it delegates to the {@link Engine} built by {@link configure}); it does NOT
619
+ * open its own connection or poll. The configured `attributes` transform runs
620
+ * once here, so every getFlag/getConfig/getExperiment reads the same bag.
621
+ *
622
+ * ```ts
623
+ * const flags = new Client(req.user);
624
+ * flags.getFlag("new_checkout"); // no user arg — bound at construction
625
+ * flags.getExperiment("price_test", { price: 9 });
626
+ * ```
627
+ */
628
+ declare class Client<U = unknown> {
629
+ private readonly engine;
630
+ /** The resolved attribute bag this handle evaluates against. */
631
+ readonly attributes: User;
632
+ constructor(user: U);
633
+ getFlag(name: string, defaultValue?: boolean): boolean;
634
+ getFlagDetail(name: string): FlagDetail;
635
+ getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
636
+ getConfig<T = unknown>(name: string, opts: GetConfigOptions<T>): T;
637
+ getExperiment<P extends Record<string, unknown>>(name: string, defaultParams: P, decode?: (raw: unknown) => P): ExperimentResult<P>;
638
+ /** Read a killswitch (not user-bound; mirrors {@link Engine.getKillswitch}). */
639
+ getKillswitch(name: string, switchKey?: string): boolean;
640
+ }
582
641
  interface SeeApi {
583
642
  /**
584
643
  * Report a handled problem and its product consequence:
@@ -630,4 +689,4 @@ interface SeeApi {
630
689
  */
631
690
  declare const see: SeeApi;
632
691
 
633
- export { ANON_ID_COOKIE, type BootstrapData, type BootstrapEmitOptions, type BootstrapPayload, type Consequence, type ExperimentResult, type ExpsBlob, FLAG_REASONS, type FetchLabelsOptions, type FlagDetail, type FlagReason, type FlagsBlob, FlagsClient, type FlagsClientEnv, type FlagsClientOptions, type GetConfigOptions, type I18nForRequest, type LabelFile, type ScriptTagSpec, type SeeApi, type SeeChain, type SeeControlFlowChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyServerConfig, type ShipeasyServerHandle, type StickyBucketStore, type StickyEntry, type User, type Violation, _murmur3ForTests, _resetShipeasyServerForTests, configureShipeasyServer, createInMemoryStickyStore, fetchLabelsForSSR, flags, getBootstrapData, getBootstrapTags, getShipeasyServerClient, i18n, isExpected, see, seeContext, shipeasy, version };
692
+ export { ANON_ID_COOKIE, type AttributesFn, type BootstrapData, type BootstrapEmitOptions, type BootstrapPayload, Client, type ConfigureOptions, type Consequence, Engine, type EngineEnv, type EngineOptions, type ExperimentResult, type ExpsBlob, FLAG_REASONS, type FetchLabelsOptions, type FlagDetail, type FlagReason, type FlagsBlob, type GetConfigOptions, type I18nForRequest, type LabelFile, type ScriptTagSpec, type SeeApi, type SeeChain, type SeeControlFlowChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyServerConfig, type ShipeasyServerHandle, type StickyBucketStore, type StickyEntry, type User, type Violation, _murmur3ForTests, _resetConfigureForTests, _resetShipeasyServerForTests, configure, configureShipeasyServer, createInMemoryStickyStore, fetchLabelsForSSR, flags, getBootstrapData, getBootstrapTags, getShipeasyServerClient, i18n, isExpected, see, seeContext, shipeasy, version };
@@ -181,7 +181,7 @@ interface StickyEntry {
181
181
  /**
182
182
  * Pluggable sticky-bucketing store for the server (doc 20 §2). Keyed by the
183
183
  * bucketing unit; the value is that unit's per-experiment assignments. Absent
184
- * from {@link FlagsClientOptions} ⇒ today's deterministic behaviour. Use
184
+ * from {@link EngineOptions} ⇒ today's deterministic behaviour. Use
185
185
  * {@link createInMemoryStickyStore} or a cookie-bridge built from request
186
186
  * cookies.
187
187
  */
@@ -198,7 +198,7 @@ interface Killswitch {
198
198
  killed: 0 | 1 | boolean;
199
199
  switches?: Record<string, 0 | 1 | boolean>;
200
200
  }
201
- /** Body of `GET /sdk/flags` — the snapshot's `flags` field. See {@link FlagsClient.fromSnapshot}. */
201
+ /** Body of `GET /sdk/flags` — the snapshot's `flags` field. See {@link Engine.fromSnapshot}. */
202
202
  interface FlagsBlob {
203
203
  version: string;
204
204
  plan: string;
@@ -208,7 +208,7 @@ interface FlagsBlob {
208
208
  }>;
209
209
  killswitches: Record<string, Killswitch>;
210
210
  }
211
- /** Body of `GET /sdk/experiments` — the snapshot's `experiments` field. See {@link FlagsClient.fromSnapshot}. */
211
+ /** Body of `GET /sdk/experiments` — the snapshot's `experiments` field. See {@link Engine.fromSnapshot}. */
212
212
  interface ExpsBlob {
213
213
  version: string;
214
214
  universes: Record<string, Universe>;
@@ -221,12 +221,12 @@ interface BootstrapPayload {
221
221
  killswitches: Record<string, boolean | Record<string, boolean>>;
222
222
  }
223
223
  declare const ANON_ID_COOKIE = "__se_anon_id";
224
- type FlagsClientEnv = "dev" | "staging" | "prod";
225
- interface FlagsClientOptions {
224
+ type EngineEnv = "dev" | "staging" | "prod";
225
+ interface EngineOptions {
226
226
  apiKey: string;
227
227
  baseUrl?: string;
228
228
  /** Which published env to read values from. Defaults to "prod". */
229
- env?: FlagsClientEnv;
229
+ env?: EngineEnv;
230
230
  /**
231
231
  * Preload the flags blob synchronously without a network fetch. Primarily
232
232
  * for tests; production callers should rely on init()/initOnce().
@@ -261,12 +261,12 @@ interface FlagsClientOptions {
261
261
  /**
262
262
  * Test mode — no network at all. init()/initOnce() are no-ops (never fetch),
263
263
  * track() is a no-op, telemetry is forced off, and the client starts
264
- * "initialized" with an empty blob. Prefer the {@link FlagsClient.forTesting}
264
+ * "initialized" with an empty blob. Prefer the {@link Engine.forTesting}
265
265
  * factory over passing this directly.
266
266
  */
267
267
  testMode?: boolean;
268
268
  }
269
- declare class FlagsClient {
269
+ declare class Engine {
270
270
  private readonly apiKey;
271
271
  private readonly baseUrl;
272
272
  private readonly env;
@@ -286,7 +286,7 @@ declare class FlagsClient {
286
286
  private readonly configOverrides;
287
287
  private readonly experimentOverrides;
288
288
  private readonly changeListeners;
289
- constructor(opts: FlagsClientOptions);
289
+ constructor(opts: EngineOptions);
290
290
  /**
291
291
  * Build a no-network, immediately-usable client for tests (Statsig-style).
292
292
  * init()/initOnce() are no-ops (never fetch), track() is a no-op, telemetry
@@ -294,12 +294,12 @@ declare class FlagsClient {
294
294
  * with overrideFlag/overrideConfig/overrideExperiment. No SDK key required.
295
295
  *
296
296
  * ```ts
297
- * const client = FlagsClient.forTesting();
297
+ * const client = Engine.forTesting();
298
298
  * client.overrideFlag("new_checkout", true);
299
299
  * client.getFlag("new_checkout", { user_id: "u1" }); // true
300
300
  * ```
301
301
  */
302
- static forTesting(opts?: Partial<FlagsClientOptions>): FlagsClient;
302
+ static forTesting(opts?: Partial<EngineOptions>): Engine;
303
303
  /**
304
304
  * Build a fully OFFLINE client from a pre-captured snapshot — no network ever.
305
305
  * Reuses the test-mode plumbing (init()/initOnce()/track() are no-ops,
@@ -313,14 +313,14 @@ declare class FlagsClient {
313
313
  static fromSnapshot(snapshot: {
314
314
  flags: FlagsBlob;
315
315
  experiments: ExpsBlob;
316
- }): FlagsClient;
316
+ }): Engine;
317
317
  /**
318
318
  * Build a fully OFFLINE client from a snapshot JSON file on disk (Node only —
319
319
  * not available in the browser entrypoint). The file must contain
320
320
  * `{ "flags": <GET /sdk/flags body>, "experiments": <GET /sdk/experiments body> }`.
321
- * See {@link FlagsClient.fromSnapshot}.
321
+ * See {@link Engine.fromSnapshot}.
322
322
  */
323
- static fromFile(path: string): FlagsClient;
323
+ static fromFile(path: string): Engine;
324
324
  init(): Promise<void>;
325
325
  initOnce(): Promise<void>;
326
326
  /** Force `getFlag(name, …)` to return `value`, ignoring the fetched gate. */
@@ -440,8 +440,8 @@ interface FetchLabelsOptions {
440
440
  timeoutMs?: number;
441
441
  }
442
442
  declare function fetchLabelsForSSR(opts: FetchLabelsOptions): Promise<LabelFile | null>;
443
- declare function configureShipeasyServer(opts: FlagsClientOptions): FlagsClient;
444
- declare function getShipeasyServerClient(): FlagsClient | null;
443
+ declare function configureShipeasyServer(opts: EngineOptions): Engine;
444
+ declare function getShipeasyServerClient(): Engine | null;
445
445
  declare function _resetShipeasyServerForTests(): void;
446
446
  interface ShipeasyServerConfig {
447
447
  /**
@@ -463,13 +463,13 @@ interface ShipeasyServerConfig {
463
463
  /**
464
464
  * Disable per-evaluation usage telemetry. ON by default. On Cloudflare
465
465
  * Workers each beacon is an outbound subrequest, so disable on hot SSR paths
466
- * that evaluate many flags per request. See {@link FlagsClientOptions.disableTelemetry}.
466
+ * that evaluate many flags per request. See {@link EngineOptions.disableTelemetry}.
467
467
  */
468
468
  disableTelemetry?: boolean;
469
469
  /**
470
470
  * Attribute names usable for targeting but never persisted in analytics
471
471
  * (LD/Statsig `privateAttributes`). Stripped from every outbound `track()`
472
- * payload. See {@link FlagsClientOptions.privateAttributes}.
472
+ * payload. See {@link EngineOptions.privateAttributes}.
473
473
  */
474
474
  privateAttributes?: string[];
475
475
  }
@@ -547,7 +547,7 @@ declare function getBootstrapData(bootstrap: BootstrapPayload | null, i18nData:
547
547
  */
548
548
  declare function getBootstrapTags(bootstrap: BootstrapPayload | null, i18nData: I18nForRequest | null, opts: BootstrapEmitOptions): string;
549
549
  declare const flags: {
550
- configure(opts: FlagsClientOptions): void;
550
+ configure(opts: EngineOptions): void;
551
551
  /**
552
552
  * Long-running server: starts the background poll. Call once at app boot.
553
553
  * Throws if the initial fetch fails (caller decides whether to crash or degrade).
@@ -570,7 +570,7 @@ declare const flags: {
570
570
  ks(name: string, switchKey?: string): boolean;
571
571
  track(userId: string, eventName: string, props?: Record<string, unknown>): void;
572
572
  /** Emit an exposure for an enrolled experiment at the decision point. See
573
- * {@link FlagsClient.logExposure}. No-op before configure(). */
573
+ * {@link Engine.logExposure}. No-op before configure(). */
574
574
  logExposure(user: string | User, name: string): void;
575
575
  /**
576
576
  * Evaluate all flags / configs / experiments for a user against the locally
@@ -579,6 +579,65 @@ declare const flags: {
579
579
  */
580
580
  evaluate(user: User, rawUrl?: string): BootstrapPayload;
581
581
  };
582
+ /** Transform YOUR application's user object into Shipeasy targeting attributes. */
583
+ type AttributesFn<U = unknown> = (user: U) => User;
584
+ interface ConfigureOptions<U = unknown> extends Omit<EngineOptions, "apiKey"> {
585
+ /** Server key — the single key the server side accepts (SHIPEASY_SERVER_KEY). */
586
+ apiKey: string;
587
+ /**
588
+ * Map your own user object into the attribute bag every flag/experiment
589
+ * evaluation sees. Runs once per `new Client(user)`. Omit when you already
590
+ * pass a plain attribute object (identity transform — the object is used
591
+ * verbatim, so it should carry `user_id`/`anonymous_id` + any targeting attrs).
592
+ */
593
+ attributes?: AttributesFn<U>;
594
+ }
595
+ /**
596
+ * Configure the SDK once at app boot, then evaluate per user with
597
+ * `new Client(user)`. Builds the process-wide {@link Engine} (polling + blob
598
+ * cache + HTTP) and registers the `attributes` transform. The first call wins;
599
+ * later calls return the existing engine (mirrors {@link configureShipeasyServer}).
600
+ *
601
+ * ```ts
602
+ * import { configure, Client } from "@shipeasy/sdk/server";
603
+ *
604
+ * configure({
605
+ * apiKey: process.env.SHIPEASY_SERVER_KEY!,
606
+ * attributes: (u: MyUser) => ({ user_id: u.id, plan: u.plan, country: u.geo.country }),
607
+ * });
608
+ *
609
+ * const flags = new Client(currentUser);
610
+ * if (flags.getFlag("new_checkout")) { ... }
611
+ * ```
612
+ */
613
+ declare function configure<U = unknown>(opts: ConfigureOptions<U>): Engine;
614
+ /** Test seam: reset the registered attribute transform. */
615
+ declare function _resetConfigureForTests(): void;
616
+ /**
617
+ * A user-bound evaluation handle. Construct one per user/request — it's cheap
618
+ * (it delegates to the {@link Engine} built by {@link configure}); it does NOT
619
+ * open its own connection or poll. The configured `attributes` transform runs
620
+ * once here, so every getFlag/getConfig/getExperiment reads the same bag.
621
+ *
622
+ * ```ts
623
+ * const flags = new Client(req.user);
624
+ * flags.getFlag("new_checkout"); // no user arg — bound at construction
625
+ * flags.getExperiment("price_test", { price: 9 });
626
+ * ```
627
+ */
628
+ declare class Client<U = unknown> {
629
+ private readonly engine;
630
+ /** The resolved attribute bag this handle evaluates against. */
631
+ readonly attributes: User;
632
+ constructor(user: U);
633
+ getFlag(name: string, defaultValue?: boolean): boolean;
634
+ getFlagDetail(name: string): FlagDetail;
635
+ getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
636
+ getConfig<T = unknown>(name: string, opts: GetConfigOptions<T>): T;
637
+ getExperiment<P extends Record<string, unknown>>(name: string, defaultParams: P, decode?: (raw: unknown) => P): ExperimentResult<P>;
638
+ /** Read a killswitch (not user-bound; mirrors {@link Engine.getKillswitch}). */
639
+ getKillswitch(name: string, switchKey?: string): boolean;
640
+ }
582
641
  interface SeeApi {
583
642
  /**
584
643
  * Report a handled problem and its product consequence:
@@ -630,4 +689,4 @@ interface SeeApi {
630
689
  */
631
690
  declare const see: SeeApi;
632
691
 
633
- export { ANON_ID_COOKIE, type BootstrapData, type BootstrapEmitOptions, type BootstrapPayload, type Consequence, type ExperimentResult, type ExpsBlob, FLAG_REASONS, type FetchLabelsOptions, type FlagDetail, type FlagReason, type FlagsBlob, FlagsClient, type FlagsClientEnv, type FlagsClientOptions, type GetConfigOptions, type I18nForRequest, type LabelFile, type ScriptTagSpec, type SeeApi, type SeeChain, type SeeControlFlowChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyServerConfig, type ShipeasyServerHandle, type StickyBucketStore, type StickyEntry, type User, type Violation, _murmur3ForTests, _resetShipeasyServerForTests, configureShipeasyServer, createInMemoryStickyStore, fetchLabelsForSSR, flags, getBootstrapData, getBootstrapTags, getShipeasyServerClient, i18n, isExpected, see, seeContext, shipeasy, version };
692
+ export { ANON_ID_COOKIE, type AttributesFn, type BootstrapData, type BootstrapEmitOptions, type BootstrapPayload, Client, type ConfigureOptions, type Consequence, Engine, type EngineEnv, type EngineOptions, type ExperimentResult, type ExpsBlob, FLAG_REASONS, type FetchLabelsOptions, type FlagDetail, type FlagReason, type FlagsBlob, type GetConfigOptions, type I18nForRequest, type LabelFile, type ScriptTagSpec, type SeeApi, type SeeChain, type SeeControlFlowChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyServerConfig, type ShipeasyServerHandle, type StickyBucketStore, type StickyEntry, type User, type Violation, _murmur3ForTests, _resetConfigureForTests, _resetShipeasyServerForTests, configure, configureShipeasyServer, createInMemoryStickyStore, fetchLabelsForSSR, flags, getBootstrapData, getBootstrapTags, getShipeasyServerClient, i18n, isExpected, see, seeContext, shipeasy, version };
@@ -31,10 +31,13 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var server_exports = {};
32
32
  __export(server_exports, {
33
33
  ANON_ID_COOKIE: () => ANON_ID_COOKIE,
34
+ Client: () => Client,
35
+ Engine: () => Engine,
34
36
  FLAG_REASONS: () => FLAG_REASONS,
35
- FlagsClient: () => FlagsClient,
36
37
  _murmur3ForTests: () => _murmur3ForTests,
38
+ _resetConfigureForTests: () => _resetConfigureForTests,
37
39
  _resetShipeasyServerForTests: () => _resetShipeasyServerForTests,
40
+ configure: () => configure,
38
41
  configureShipeasyServer: () => configureShipeasyServer,
39
42
  createInMemoryStickyStore: () => createInMemoryStickyStore,
40
43
  fetchLabelsForSSR: () => fetchLabelsForSSR,
@@ -543,7 +546,7 @@ function parseOverrides(rawUrl) {
543
546
  }
544
547
  return { gates, configs, experiments };
545
548
  }
546
- var FlagsClient = class _FlagsClient {
549
+ var Engine = class _Engine {
547
550
  apiKey;
548
551
  baseUrl;
549
552
  env;
@@ -558,7 +561,7 @@ var FlagsClient = class _FlagsClient {
558
561
  pollInterval = 30;
559
562
  timer = null;
560
563
  initialized = false;
561
- // Test mode: built by `FlagsClient.forTesting()`. When set, init()/initOnce()
564
+ // Test mode: built by `Engine.forTesting()`. When set, init()/initOnce()
562
565
  // never fetch, track() is a no-op, and telemetry is off — the client is a
563
566
  // fully self-contained, network-free seam for unit tests.
564
567
  testMode;
@@ -599,13 +602,13 @@ var FlagsClient = class _FlagsClient {
599
602
  * with overrideFlag/overrideConfig/overrideExperiment. No SDK key required.
600
603
  *
601
604
  * ```ts
602
- * const client = FlagsClient.forTesting();
605
+ * const client = Engine.forTesting();
603
606
  * client.overrideFlag("new_checkout", true);
604
607
  * client.getFlag("new_checkout", { user_id: "u1" }); // true
605
608
  * ```
606
609
  */
607
610
  static forTesting(opts) {
608
- return new _FlagsClient({ apiKey: "", ...opts, testMode: true });
611
+ return new _Engine({ apiKey: "", ...opts, testMode: true });
609
612
  }
610
613
  /**
611
614
  * Build a fully OFFLINE client from a pre-captured snapshot — no network ever.
@@ -618,7 +621,7 @@ var FlagsClient = class _FlagsClient {
618
621
  * `{ flags: <GET /sdk/flags body>, experiments: <GET /sdk/experiments body> }`.
619
622
  */
620
623
  static fromSnapshot(snapshot) {
621
- const client = new _FlagsClient({ apiKey: "", testMode: true });
624
+ const client = new _Engine({ apiKey: "", testMode: true });
622
625
  client.flagsBlob = snapshot.flags;
623
626
  client.expsBlob = snapshot.experiments;
624
627
  client.initialized = true;
@@ -628,13 +631,13 @@ var FlagsClient = class _FlagsClient {
628
631
  * Build a fully OFFLINE client from a snapshot JSON file on disk (Node only —
629
632
  * not available in the browser entrypoint). The file must contain
630
633
  * `{ "flags": <GET /sdk/flags body>, "experiments": <GET /sdk/experiments body> }`.
631
- * See {@link FlagsClient.fromSnapshot}.
634
+ * See {@link Engine.fromSnapshot}.
632
635
  */
633
636
  static fromFile(path) {
634
637
  const fs = require("fs");
635
638
  const raw = fs.readFileSync(path, "utf8");
636
639
  const snapshot = JSON.parse(raw);
637
- return _FlagsClient.fromSnapshot(snapshot);
640
+ return _Engine.fromSnapshot(snapshot);
638
641
  }
639
642
  async init() {
640
643
  if (this.testMode) {
@@ -1094,7 +1097,7 @@ async function fetchLabelsForSSR(opts) {
1094
1097
  var _server = null;
1095
1098
  function configureShipeasyServer(opts) {
1096
1099
  if (_server) return _server;
1097
- _server = new FlagsClient(opts);
1100
+ _server = new Engine(opts);
1098
1101
  return _server;
1099
1102
  }
1100
1103
  function getShipeasyServerClient() {
@@ -1269,7 +1272,7 @@ var flags = {
1269
1272
  _server?.track(userId, eventName, props);
1270
1273
  },
1271
1274
  /** Emit an exposure for an enrolled experiment at the decision point. See
1272
- * {@link FlagsClient.logExposure}. No-op before configure(). */
1275
+ * {@link Engine.logExposure}. No-op before configure(). */
1273
1276
  logExposure(user, name) {
1274
1277
  _server?.logExposure(user, name);
1275
1278
  },
@@ -1287,6 +1290,50 @@ var flags = {
1287
1290
  };
1288
1291
  }
1289
1292
  };
1293
+ var _identityAttributes = (user) => user && typeof user === "object" ? user : {};
1294
+ var _attributes = _identityAttributes;
1295
+ function configure(opts) {
1296
+ const { attributes, ...engineOpts } = opts;
1297
+ _attributes = attributes ?? _identityAttributes;
1298
+ const engine = configureShipeasyServer(engineOpts);
1299
+ void engine.initOnce().catch(() => {
1300
+ });
1301
+ return engine;
1302
+ }
1303
+ function _resetConfigureForTests() {
1304
+ _attributes = _identityAttributes;
1305
+ }
1306
+ var Client = class {
1307
+ engine;
1308
+ /** The resolved attribute bag this handle evaluates against. */
1309
+ attributes;
1310
+ constructor(user) {
1311
+ const engine = getShipeasyServerClient();
1312
+ if (!engine) {
1313
+ throw new Error(
1314
+ "[shipeasy] new Client(user) called before configure({ apiKey }). Call configure() once at app boot from @shipeasy/sdk/server."
1315
+ );
1316
+ }
1317
+ this.engine = engine;
1318
+ this.attributes = _attributes(user);
1319
+ }
1320
+ getFlag(name, defaultValue = false) {
1321
+ return this.engine.getFlag(name, this.attributes, defaultValue);
1322
+ }
1323
+ getFlagDetail(name) {
1324
+ return this.engine.getFlagDetail(name, this.attributes);
1325
+ }
1326
+ getConfig(name, decodeOrOpts) {
1327
+ return this.engine.getConfig(name, decodeOrOpts);
1328
+ }
1329
+ getExperiment(name, defaultParams, decode) {
1330
+ return this.engine.getExperiment(name, this.attributes, defaultParams, decode);
1331
+ }
1332
+ /** Read a killswitch (not user-bound; mirrors {@link Engine.getKillswitch}). */
1333
+ getKillswitch(name, switchKey) {
1334
+ return this.engine.getKillswitch(name, switchKey);
1335
+ }
1336
+ };
1290
1337
  function dispatchSee(problem, consequence, extras, kind) {
1291
1338
  if (!_server) {
1292
1339
  console.warn("[shipeasy] see() called before shipeasy({ serverKey }) \u2014 error dropped");
@@ -1304,10 +1351,13 @@ var see = Object.assign(
1304
1351
  // Annotate the CommonJS export names for ESM import in node:
1305
1352
  0 && (module.exports = {
1306
1353
  ANON_ID_COOKIE,
1354
+ Client,
1355
+ Engine,
1307
1356
  FLAG_REASONS,
1308
- FlagsClient,
1309
1357
  _murmur3ForTests,
1358
+ _resetConfigureForTests,
1310
1359
  _resetShipeasyServerForTests,
1360
+ configure,
1311
1361
  configureShipeasyServer,
1312
1362
  createInMemoryStickyStore,
1313
1363
  fetchLabelsForSSR,