@shipeasy/sdk 5.0.0 → 5.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,338 @@
1
+ import { Provider, EvaluationContext, Logger, ResolutionDetails, JsonValue } from '@openfeature/server-sdk';
2
+
3
+ type SeeExtras = Record<string, string | number | boolean | null | undefined>;
4
+ type SeeKind = "caught" | "uncaught" | "unhandled_rejection" | "network" | "violation";
5
+ /** Built by `causesThe(subject).to(outcome)` — never constructed by hand. */
6
+ interface Consequence {
7
+ readonly __seConsequence: true;
8
+ readonly subject: string;
9
+ readonly outcome: string;
10
+ }
11
+
12
+ interface User {
13
+ user_id?: string;
14
+ anonymous_id?: string;
15
+ [attr: string]: unknown;
16
+ }
17
+ interface ExperimentResult<P> {
18
+ inExperiment: boolean;
19
+ group: string;
20
+ params: P;
21
+ }
22
+ /**
23
+ * Why a flag evaluated the way it did (LaunchDarkly variationDetail parity).
24
+ * Computed at the client boundary, never inside the canonical eval:
25
+ * - CLIENT_NOT_READY — no rules blob loaded yet (init()/initOnce() pending)
26
+ * - FLAG_NOT_FOUND — the gate name isn't present in the loaded blob
27
+ * - OFF — the gate exists but is disabled / killed
28
+ * - OVERRIDE — a local override (overrideFlag) decided the value
29
+ * - RULE_MATCH — the gate evaluated true (rules + rollout passed)
30
+ * - DEFAULT — the gate evaluated false (a rule or the rollout denied)
31
+ */
32
+ declare const FLAG_REASONS: readonly ["CLIENT_NOT_READY", "FLAG_NOT_FOUND", "OFF", "OVERRIDE", "RULE_MATCH", "DEFAULT"];
33
+ type FlagReason = (typeof FLAG_REASONS)[number];
34
+ interface FlagDetail {
35
+ value: boolean;
36
+ reason: FlagReason;
37
+ }
38
+ /** Options object form of `getConfig` — keeps the legacy `decode` callback and
39
+ * adds a `defaultValue` returned when the config key is absent. */
40
+ interface GetConfigOptions<T = unknown> {
41
+ /** Decode the raw stored value into the typed shape callers want. */
42
+ decode?: (raw: unknown) => T;
43
+ /** Returned when the config key is absent (not overridden, not in the blob). */
44
+ defaultValue?: T;
45
+ }
46
+ interface GateRule {
47
+ attr: string;
48
+ op: "eq" | "neq" | "in" | "not_in" | "gt" | "gte" | "lt" | "lte" | "contains" | "regex";
49
+ value: unknown;
50
+ }
51
+ interface Gate {
52
+ rules: GateRule[];
53
+ rolloutPct: number;
54
+ salt: string;
55
+ enabled: 0 | 1 | boolean;
56
+ killswitch?: 0 | 1 | boolean;
57
+ }
58
+ interface ExperimentGroup {
59
+ name: string;
60
+ weight: number;
61
+ params: Record<string, unknown>;
62
+ }
63
+ interface Experiment {
64
+ universe: string;
65
+ targetingGate?: string | null;
66
+ allocationPct: number;
67
+ salt: string;
68
+ groups: ExperimentGroup[];
69
+ status: "draft" | "running" | "stopped" | "archived";
70
+ /** Attribute to bucket on (e.g. company_id); defaults to user_id/anonymous_id. */
71
+ bucketBy?: string | null;
72
+ }
73
+ /** One persisted sticky assignment: group + 8-char salt prefix (reshuffle key). */
74
+ interface StickyEntry {
75
+ g: string;
76
+ s: string;
77
+ }
78
+ /**
79
+ * Pluggable sticky-bucketing store for the server (doc 20 §2). Keyed by the
80
+ * bucketing unit; the value is that unit's per-experiment assignments. Absent
81
+ * from {@link FlagsClientOptions} ⇒ today's deterministic behaviour. Use
82
+ * {@link createInMemoryStickyStore} or a cookie-bridge built from request
83
+ * cookies.
84
+ */
85
+ interface StickyBucketStore {
86
+ get(unit: string): Record<string, StickyEntry> | undefined;
87
+ set(unit: string, exp: string, entry: StickyEntry): void;
88
+ }
89
+ interface Universe {
90
+ holdout_range: [number, number] | null;
91
+ }
92
+ interface Killswitch {
93
+ killed: 0 | 1 | boolean;
94
+ switches?: Record<string, 0 | 1 | boolean>;
95
+ }
96
+ /** Body of `GET /sdk/flags` — the snapshot's `flags` field. See {@link FlagsClient.fromSnapshot}. */
97
+ interface FlagsBlob {
98
+ version: string;
99
+ plan: string;
100
+ gates: Record<string, Gate>;
101
+ configs: Record<string, {
102
+ value: unknown;
103
+ }>;
104
+ killswitches: Record<string, Killswitch>;
105
+ }
106
+ /** Body of `GET /sdk/experiments` — the snapshot's `experiments` field. See {@link FlagsClient.fromSnapshot}. */
107
+ interface ExpsBlob {
108
+ version: string;
109
+ universes: Record<string, Universe>;
110
+ experiments: Record<string, Experiment>;
111
+ }
112
+ interface BootstrapPayload {
113
+ flags: Record<string, boolean>;
114
+ configs: Record<string, unknown>;
115
+ experiments: Record<string, ExperimentResult<Record<string, unknown>>>;
116
+ killswitches: Record<string, boolean | Record<string, boolean>>;
117
+ }
118
+ type FlagsClientEnv = "dev" | "staging" | "prod";
119
+ interface FlagsClientOptions {
120
+ apiKey: string;
121
+ baseUrl?: string;
122
+ /** Which published env to read values from. Defaults to "prod". */
123
+ env?: FlagsClientEnv;
124
+ /**
125
+ * Preload the flags blob synchronously without a network fetch. Primarily
126
+ * for tests; production callers should rely on init()/initOnce().
127
+ */
128
+ initialBlob?: FlagsBlob;
129
+ /**
130
+ * Per-evaluation usage telemetry. ON by default — each getFlag/getConfig/
131
+ * getExperiment/getKillswitch (and the per-key evaluate() loop) fires one
132
+ * fire-and-forget beacon counted by Cloudflare's native per-path analytics.
133
+ * Pass `true` to disable. NOTE: on Cloudflare Workers each beacon is an
134
+ * outbound subrequest (cap 50 free / 1000 paid per invocation), so disable
135
+ * this on hot request paths that evaluate many flags per request.
136
+ */
137
+ disableTelemetry?: boolean;
138
+ /** Override the telemetry beacon host. Defaults to {@link DEFAULT_TELEMETRY_URL}. */
139
+ telemetryUrl?: string;
140
+ /**
141
+ * Attribute names usable for targeting but never persisted in analytics
142
+ * (LD/Statsig `privateAttributes`). The server evaluates locally so private
143
+ * attrs never leave for evaluation at all; the only egress is `/collect`, and
144
+ * the listed keys are stripped from every outbound `track()` payload.
145
+ */
146
+ privateAttributes?: string[];
147
+ /**
148
+ * Sticky-bucketing store (doc 20 §2). When provided, `getExperiment` locks a
149
+ * unit to its first-assigned variant — changing allocation % or weights won't
150
+ * re-bucket enrolled units (changing the experiment salt is the reshuffle
151
+ * lever). Absent ⇒ deterministic (fully backward compatible). Built-ins:
152
+ * {@link createInMemoryStickyStore}, or a cookie-bridge over `__se_sticky`.
153
+ */
154
+ stickyStore?: StickyBucketStore;
155
+ /**
156
+ * Test mode — no network at all. init()/initOnce() are no-ops (never fetch),
157
+ * track() is a no-op, telemetry is forced off, and the client starts
158
+ * "initialized" with an empty blob. Prefer the {@link FlagsClient.forTesting}
159
+ * factory over passing this directly.
160
+ */
161
+ testMode?: boolean;
162
+ }
163
+ declare class FlagsClient {
164
+ private readonly apiKey;
165
+ private readonly baseUrl;
166
+ private readonly env;
167
+ private readonly privateAttributes;
168
+ private readonly stickyStore;
169
+ private readonly telemetry;
170
+ private readonly seeLimiter;
171
+ private flagsBlob;
172
+ private expsBlob;
173
+ private flagsEtag;
174
+ private expsEtag;
175
+ private pollInterval;
176
+ private timer;
177
+ private initialized;
178
+ private readonly testMode;
179
+ private readonly flagOverrides;
180
+ private readonly configOverrides;
181
+ private readonly experimentOverrides;
182
+ private readonly changeListeners;
183
+ constructor(opts: FlagsClientOptions);
184
+ /**
185
+ * Build a no-network, immediately-usable client for tests (Statsig-style).
186
+ * init()/initOnce() are no-ops (never fetch), track() is a no-op, telemetry
187
+ * is disabled, and the client is already "initialized" — seed every entity
188
+ * with overrideFlag/overrideConfig/overrideExperiment. No SDK key required.
189
+ *
190
+ * ```ts
191
+ * const client = FlagsClient.forTesting();
192
+ * client.overrideFlag("new_checkout", true);
193
+ * client.getFlag("new_checkout", { user_id: "u1" }); // true
194
+ * ```
195
+ */
196
+ static forTesting(opts?: Partial<FlagsClientOptions>): FlagsClient;
197
+ /**
198
+ * Build a fully OFFLINE client from a pre-captured snapshot — no network ever.
199
+ * Reuses the test-mode plumbing (init()/initOnce()/track() are no-ops,
200
+ * telemetry off) but, unlike forTesting(), seeds the REAL flags + experiments
201
+ * blobs so evaluations run the canonical eval against the snapshot. Local
202
+ * overrides still apply on top.
203
+ *
204
+ * Snapshot shape mirrors the wire bodies:
205
+ * `{ flags: <GET /sdk/flags body>, experiments: <GET /sdk/experiments body> }`.
206
+ */
207
+ static fromSnapshot(snapshot: {
208
+ flags: FlagsBlob;
209
+ experiments: ExpsBlob;
210
+ }): FlagsClient;
211
+ /**
212
+ * Build a fully OFFLINE client from a snapshot JSON file on disk (Node only —
213
+ * not available in the browser entrypoint). The file must contain
214
+ * `{ "flags": <GET /sdk/flags body>, "experiments": <GET /sdk/experiments body> }`.
215
+ * See {@link FlagsClient.fromSnapshot}.
216
+ */
217
+ static fromFile(path: string): FlagsClient;
218
+ init(): Promise<void>;
219
+ initOnce(): Promise<void>;
220
+ /** Force `getFlag(name, …)` to return `value`, ignoring the fetched gate. */
221
+ overrideFlag(name: string, value: boolean): void;
222
+ /** Force `getConfig(name)` to return `value`, ignoring the fetched config. */
223
+ overrideConfig(name: string, value: unknown): void;
224
+ /**
225
+ * Force `getExperiment(name, …)` to return `{ inExperiment: true, group, params }`,
226
+ * ignoring allocation, holdouts, and targeting.
227
+ */
228
+ overrideExperiment(name: string, group: string, params: Record<string, unknown>): void;
229
+ /** Remove every programmatic override set via the override* methods. */
230
+ clearOverrides(): void;
231
+ destroy(): void;
232
+ /**
233
+ * Subscribe to data-change notifications. The listener fires after a
234
+ * background poll fetch returns NEW data (200, not 304) — i.e. after the
235
+ * cached blob is updated. Never fires in testMode/offline (no polling).
236
+ * Returns an unsubscribe function.
237
+ */
238
+ onChange(listener: () => void): () => void;
239
+ private notifyChange;
240
+ private startPoll;
241
+ private fetchAll;
242
+ private fetchFlags;
243
+ private fetchExps;
244
+ /**
245
+ * Evaluate a gate and report WHY (LaunchDarkly variationDetail parity). The
246
+ * reason is computed entirely at this boundary — the canonical eval
247
+ * (evalGateInternal) is untouched. A local override short-circuits BEFORE
248
+ * telemetry, exactly like getFlag's override path; otherwise exactly one
249
+ * "gate" beacon is emitted.
250
+ */
251
+ getFlagDetail(name: string, user: User): FlagDetail;
252
+ /**
253
+ * Read a feature gate. Returns `defaultValue` ONLY when the gate cannot be
254
+ * evaluated (client not initialized or flag not found) — never for a gate that
255
+ * legitimately evaluates to false. Plain `getFlag(name, user)` keeps returning
256
+ * false for a missing flag.
257
+ */
258
+ getFlag(name: string, user: User, defaultValue?: boolean): boolean;
259
+ getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
260
+ getConfig<T = unknown>(name: string, opts: GetConfigOptions<T>): T;
261
+ getExperiment<P extends Record<string, unknown>>(name: string, user: User, defaultParams: P, decode?: (raw: unknown) => P): ExperimentResult<P>;
262
+ /** Drop caller-marked private attributes from an outbound props bag. */
263
+ private stripPrivate;
264
+ track(userId: string, eventName: string, props?: Record<string, unknown>): void;
265
+ /**
266
+ * Emit an exposure event for an experiment at the server-side decision point
267
+ * (parity with the browser's auto-exposure). The server is stateless and
268
+ * never auto-logs, so call this when you actually present the treatment.
269
+ * Re-evaluates the experiment for `user` (a bare `user_id` string is wrapped
270
+ * as `{ user_id }`); if the user is enrolled, POSTs a single exposure to
271
+ * `/collect`. No-op in test mode or when the user isn't enrolled.
272
+ */
273
+ logExposure(user: string | User, name: string): void;
274
+ /**
275
+ * Report a structured error into the errors primitive. Fire-and-forget —
276
+ * never blocks or throws into the request path. Spam-guarded by a 30s
277
+ * dedup window + per-process cap.
278
+ */
279
+ reportError(problem: unknown, consequence: Consequence, extras?: SeeExtras, kind?: SeeKind): void;
280
+ /**
281
+ * Evaluate all flags, configs, and experiments for a user against the locally
282
+ * cached blob (no network call). Applies ?se_ks_* / ?se_cf_* / ?se_exp_*
283
+ * overrides from the request URL when provided.
284
+ *
285
+ * Intended for SSR: call on the server, inject the result as
286
+ * `window.__SE_BOOTSTRAP` in the HTML, and the client SDK will read it
287
+ * synchronously without waiting for identify() to resolve.
288
+ */
289
+ evaluate(user: User, rawUrl?: string): BootstrapPayload;
290
+ getKillswitch(name: string, switchKey?: string): boolean;
291
+ }
292
+
293
+ /**
294
+ * OpenFeature **server** provider for Shipeasy.
295
+ *
296
+ * Lets apps standardized on the CNCF OpenFeature API plug Shipeasy in as the
297
+ * backing provider:
298
+ *
299
+ * ```ts
300
+ * import { OpenFeature } from "@openfeature/server-sdk";
301
+ * import { FlagsClient } from "@shipeasy/sdk/server";
302
+ * import { ShipeasyProvider } from "@shipeasy/sdk/openfeature-server";
303
+ *
304
+ * const client = new FlagsClient({ apiKey: process.env.SHIPEASY_SERVER_KEY! });
305
+ * await OpenFeature.setProviderAndWait(new ShipeasyProvider(client));
306
+ *
307
+ * const ofClient = OpenFeature.getClient();
308
+ * const on = await ofClient.getBooleanValue("new_checkout", false, { targetingKey: "u1" });
309
+ * ```
310
+ *
311
+ * Pure adapter over `FlagsClient` — no change to evaluation. `@openfeature/server-sdk`
312
+ * is an optional peer dependency; install it in the consuming app.
313
+ */
314
+
315
+ /**
316
+ * Shipeasy OpenFeature provider (server paradigm). Wraps a `FlagsClient`;
317
+ * evaluation is local against the cached blob, so resolution is effectively
318
+ * synchronous (the methods are `async` only to satisfy the server contract).
319
+ */
320
+ declare class ShipeasyProvider implements Provider {
321
+ private readonly client;
322
+ readonly metadata: {
323
+ readonly name: "shipeasy";
324
+ };
325
+ readonly runsOn: "server";
326
+ constructor(client: FlagsClient);
327
+ /** Fetch the rules blob once. The SDK fires `Ready` when this resolves. */
328
+ initialize(): Promise<void>;
329
+ onClose(): Promise<void>;
330
+ resolveBooleanEvaluation(flagKey: string, defaultValue: boolean, context: EvaluationContext, _logger?: Logger): Promise<ResolutionDetails<boolean>>;
331
+ resolveStringEvaluation(flagKey: string, defaultValue: string, _context: EvaluationContext, _logger?: Logger): Promise<ResolutionDetails<string>>;
332
+ resolveNumberEvaluation(flagKey: string, defaultValue: number, _context: EvaluationContext, _logger?: Logger): Promise<ResolutionDetails<number>>;
333
+ resolveObjectEvaluation<T extends JsonValue>(flagKey: string, defaultValue: T, _context: EvaluationContext, _logger?: Logger): Promise<ResolutionDetails<T>>;
334
+ /** OpenFeature `track()` → Shipeasy `track()`. No-ops without a targeting key. */
335
+ track(trackingEventName: string, context: EvaluationContext, details?: Record<string, unknown>): void;
336
+ }
337
+
338
+ export { ShipeasyProvider };
@@ -0,0 +1,338 @@
1
+ import { Provider, EvaluationContext, Logger, ResolutionDetails, JsonValue } from '@openfeature/server-sdk';
2
+
3
+ type SeeExtras = Record<string, string | number | boolean | null | undefined>;
4
+ type SeeKind = "caught" | "uncaught" | "unhandled_rejection" | "network" | "violation";
5
+ /** Built by `causesThe(subject).to(outcome)` — never constructed by hand. */
6
+ interface Consequence {
7
+ readonly __seConsequence: true;
8
+ readonly subject: string;
9
+ readonly outcome: string;
10
+ }
11
+
12
+ interface User {
13
+ user_id?: string;
14
+ anonymous_id?: string;
15
+ [attr: string]: unknown;
16
+ }
17
+ interface ExperimentResult<P> {
18
+ inExperiment: boolean;
19
+ group: string;
20
+ params: P;
21
+ }
22
+ /**
23
+ * Why a flag evaluated the way it did (LaunchDarkly variationDetail parity).
24
+ * Computed at the client boundary, never inside the canonical eval:
25
+ * - CLIENT_NOT_READY — no rules blob loaded yet (init()/initOnce() pending)
26
+ * - FLAG_NOT_FOUND — the gate name isn't present in the loaded blob
27
+ * - OFF — the gate exists but is disabled / killed
28
+ * - OVERRIDE — a local override (overrideFlag) decided the value
29
+ * - RULE_MATCH — the gate evaluated true (rules + rollout passed)
30
+ * - DEFAULT — the gate evaluated false (a rule or the rollout denied)
31
+ */
32
+ declare const FLAG_REASONS: readonly ["CLIENT_NOT_READY", "FLAG_NOT_FOUND", "OFF", "OVERRIDE", "RULE_MATCH", "DEFAULT"];
33
+ type FlagReason = (typeof FLAG_REASONS)[number];
34
+ interface FlagDetail {
35
+ value: boolean;
36
+ reason: FlagReason;
37
+ }
38
+ /** Options object form of `getConfig` — keeps the legacy `decode` callback and
39
+ * adds a `defaultValue` returned when the config key is absent. */
40
+ interface GetConfigOptions<T = unknown> {
41
+ /** Decode the raw stored value into the typed shape callers want. */
42
+ decode?: (raw: unknown) => T;
43
+ /** Returned when the config key is absent (not overridden, not in the blob). */
44
+ defaultValue?: T;
45
+ }
46
+ interface GateRule {
47
+ attr: string;
48
+ op: "eq" | "neq" | "in" | "not_in" | "gt" | "gte" | "lt" | "lte" | "contains" | "regex";
49
+ value: unknown;
50
+ }
51
+ interface Gate {
52
+ rules: GateRule[];
53
+ rolloutPct: number;
54
+ salt: string;
55
+ enabled: 0 | 1 | boolean;
56
+ killswitch?: 0 | 1 | boolean;
57
+ }
58
+ interface ExperimentGroup {
59
+ name: string;
60
+ weight: number;
61
+ params: Record<string, unknown>;
62
+ }
63
+ interface Experiment {
64
+ universe: string;
65
+ targetingGate?: string | null;
66
+ allocationPct: number;
67
+ salt: string;
68
+ groups: ExperimentGroup[];
69
+ status: "draft" | "running" | "stopped" | "archived";
70
+ /** Attribute to bucket on (e.g. company_id); defaults to user_id/anonymous_id. */
71
+ bucketBy?: string | null;
72
+ }
73
+ /** One persisted sticky assignment: group + 8-char salt prefix (reshuffle key). */
74
+ interface StickyEntry {
75
+ g: string;
76
+ s: string;
77
+ }
78
+ /**
79
+ * Pluggable sticky-bucketing store for the server (doc 20 §2). Keyed by the
80
+ * bucketing unit; the value is that unit's per-experiment assignments. Absent
81
+ * from {@link FlagsClientOptions} ⇒ today's deterministic behaviour. Use
82
+ * {@link createInMemoryStickyStore} or a cookie-bridge built from request
83
+ * cookies.
84
+ */
85
+ interface StickyBucketStore {
86
+ get(unit: string): Record<string, StickyEntry> | undefined;
87
+ set(unit: string, exp: string, entry: StickyEntry): void;
88
+ }
89
+ interface Universe {
90
+ holdout_range: [number, number] | null;
91
+ }
92
+ interface Killswitch {
93
+ killed: 0 | 1 | boolean;
94
+ switches?: Record<string, 0 | 1 | boolean>;
95
+ }
96
+ /** Body of `GET /sdk/flags` — the snapshot's `flags` field. See {@link FlagsClient.fromSnapshot}. */
97
+ interface FlagsBlob {
98
+ version: string;
99
+ plan: string;
100
+ gates: Record<string, Gate>;
101
+ configs: Record<string, {
102
+ value: unknown;
103
+ }>;
104
+ killswitches: Record<string, Killswitch>;
105
+ }
106
+ /** Body of `GET /sdk/experiments` — the snapshot's `experiments` field. See {@link FlagsClient.fromSnapshot}. */
107
+ interface ExpsBlob {
108
+ version: string;
109
+ universes: Record<string, Universe>;
110
+ experiments: Record<string, Experiment>;
111
+ }
112
+ interface BootstrapPayload {
113
+ flags: Record<string, boolean>;
114
+ configs: Record<string, unknown>;
115
+ experiments: Record<string, ExperimentResult<Record<string, unknown>>>;
116
+ killswitches: Record<string, boolean | Record<string, boolean>>;
117
+ }
118
+ type FlagsClientEnv = "dev" | "staging" | "prod";
119
+ interface FlagsClientOptions {
120
+ apiKey: string;
121
+ baseUrl?: string;
122
+ /** Which published env to read values from. Defaults to "prod". */
123
+ env?: FlagsClientEnv;
124
+ /**
125
+ * Preload the flags blob synchronously without a network fetch. Primarily
126
+ * for tests; production callers should rely on init()/initOnce().
127
+ */
128
+ initialBlob?: FlagsBlob;
129
+ /**
130
+ * Per-evaluation usage telemetry. ON by default — each getFlag/getConfig/
131
+ * getExperiment/getKillswitch (and the per-key evaluate() loop) fires one
132
+ * fire-and-forget beacon counted by Cloudflare's native per-path analytics.
133
+ * Pass `true` to disable. NOTE: on Cloudflare Workers each beacon is an
134
+ * outbound subrequest (cap 50 free / 1000 paid per invocation), so disable
135
+ * this on hot request paths that evaluate many flags per request.
136
+ */
137
+ disableTelemetry?: boolean;
138
+ /** Override the telemetry beacon host. Defaults to {@link DEFAULT_TELEMETRY_URL}. */
139
+ telemetryUrl?: string;
140
+ /**
141
+ * Attribute names usable for targeting but never persisted in analytics
142
+ * (LD/Statsig `privateAttributes`). The server evaluates locally so private
143
+ * attrs never leave for evaluation at all; the only egress is `/collect`, and
144
+ * the listed keys are stripped from every outbound `track()` payload.
145
+ */
146
+ privateAttributes?: string[];
147
+ /**
148
+ * Sticky-bucketing store (doc 20 §2). When provided, `getExperiment` locks a
149
+ * unit to its first-assigned variant — changing allocation % or weights won't
150
+ * re-bucket enrolled units (changing the experiment salt is the reshuffle
151
+ * lever). Absent ⇒ deterministic (fully backward compatible). Built-ins:
152
+ * {@link createInMemoryStickyStore}, or a cookie-bridge over `__se_sticky`.
153
+ */
154
+ stickyStore?: StickyBucketStore;
155
+ /**
156
+ * Test mode — no network at all. init()/initOnce() are no-ops (never fetch),
157
+ * track() is a no-op, telemetry is forced off, and the client starts
158
+ * "initialized" with an empty blob. Prefer the {@link FlagsClient.forTesting}
159
+ * factory over passing this directly.
160
+ */
161
+ testMode?: boolean;
162
+ }
163
+ declare class FlagsClient {
164
+ private readonly apiKey;
165
+ private readonly baseUrl;
166
+ private readonly env;
167
+ private readonly privateAttributes;
168
+ private readonly stickyStore;
169
+ private readonly telemetry;
170
+ private readonly seeLimiter;
171
+ private flagsBlob;
172
+ private expsBlob;
173
+ private flagsEtag;
174
+ private expsEtag;
175
+ private pollInterval;
176
+ private timer;
177
+ private initialized;
178
+ private readonly testMode;
179
+ private readonly flagOverrides;
180
+ private readonly configOverrides;
181
+ private readonly experimentOverrides;
182
+ private readonly changeListeners;
183
+ constructor(opts: FlagsClientOptions);
184
+ /**
185
+ * Build a no-network, immediately-usable client for tests (Statsig-style).
186
+ * init()/initOnce() are no-ops (never fetch), track() is a no-op, telemetry
187
+ * is disabled, and the client is already "initialized" — seed every entity
188
+ * with overrideFlag/overrideConfig/overrideExperiment. No SDK key required.
189
+ *
190
+ * ```ts
191
+ * const client = FlagsClient.forTesting();
192
+ * client.overrideFlag("new_checkout", true);
193
+ * client.getFlag("new_checkout", { user_id: "u1" }); // true
194
+ * ```
195
+ */
196
+ static forTesting(opts?: Partial<FlagsClientOptions>): FlagsClient;
197
+ /**
198
+ * Build a fully OFFLINE client from a pre-captured snapshot — no network ever.
199
+ * Reuses the test-mode plumbing (init()/initOnce()/track() are no-ops,
200
+ * telemetry off) but, unlike forTesting(), seeds the REAL flags + experiments
201
+ * blobs so evaluations run the canonical eval against the snapshot. Local
202
+ * overrides still apply on top.
203
+ *
204
+ * Snapshot shape mirrors the wire bodies:
205
+ * `{ flags: <GET /sdk/flags body>, experiments: <GET /sdk/experiments body> }`.
206
+ */
207
+ static fromSnapshot(snapshot: {
208
+ flags: FlagsBlob;
209
+ experiments: ExpsBlob;
210
+ }): FlagsClient;
211
+ /**
212
+ * Build a fully OFFLINE client from a snapshot JSON file on disk (Node only —
213
+ * not available in the browser entrypoint). The file must contain
214
+ * `{ "flags": <GET /sdk/flags body>, "experiments": <GET /sdk/experiments body> }`.
215
+ * See {@link FlagsClient.fromSnapshot}.
216
+ */
217
+ static fromFile(path: string): FlagsClient;
218
+ init(): Promise<void>;
219
+ initOnce(): Promise<void>;
220
+ /** Force `getFlag(name, …)` to return `value`, ignoring the fetched gate. */
221
+ overrideFlag(name: string, value: boolean): void;
222
+ /** Force `getConfig(name)` to return `value`, ignoring the fetched config. */
223
+ overrideConfig(name: string, value: unknown): void;
224
+ /**
225
+ * Force `getExperiment(name, …)` to return `{ inExperiment: true, group, params }`,
226
+ * ignoring allocation, holdouts, and targeting.
227
+ */
228
+ overrideExperiment(name: string, group: string, params: Record<string, unknown>): void;
229
+ /** Remove every programmatic override set via the override* methods. */
230
+ clearOverrides(): void;
231
+ destroy(): void;
232
+ /**
233
+ * Subscribe to data-change notifications. The listener fires after a
234
+ * background poll fetch returns NEW data (200, not 304) — i.e. after the
235
+ * cached blob is updated. Never fires in testMode/offline (no polling).
236
+ * Returns an unsubscribe function.
237
+ */
238
+ onChange(listener: () => void): () => void;
239
+ private notifyChange;
240
+ private startPoll;
241
+ private fetchAll;
242
+ private fetchFlags;
243
+ private fetchExps;
244
+ /**
245
+ * Evaluate a gate and report WHY (LaunchDarkly variationDetail parity). The
246
+ * reason is computed entirely at this boundary — the canonical eval
247
+ * (evalGateInternal) is untouched. A local override short-circuits BEFORE
248
+ * telemetry, exactly like getFlag's override path; otherwise exactly one
249
+ * "gate" beacon is emitted.
250
+ */
251
+ getFlagDetail(name: string, user: User): FlagDetail;
252
+ /**
253
+ * Read a feature gate. Returns `defaultValue` ONLY when the gate cannot be
254
+ * evaluated (client not initialized or flag not found) — never for a gate that
255
+ * legitimately evaluates to false. Plain `getFlag(name, user)` keeps returning
256
+ * false for a missing flag.
257
+ */
258
+ getFlag(name: string, user: User, defaultValue?: boolean): boolean;
259
+ getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
260
+ getConfig<T = unknown>(name: string, opts: GetConfigOptions<T>): T;
261
+ getExperiment<P extends Record<string, unknown>>(name: string, user: User, defaultParams: P, decode?: (raw: unknown) => P): ExperimentResult<P>;
262
+ /** Drop caller-marked private attributes from an outbound props bag. */
263
+ private stripPrivate;
264
+ track(userId: string, eventName: string, props?: Record<string, unknown>): void;
265
+ /**
266
+ * Emit an exposure event for an experiment at the server-side decision point
267
+ * (parity with the browser's auto-exposure). The server is stateless and
268
+ * never auto-logs, so call this when you actually present the treatment.
269
+ * Re-evaluates the experiment for `user` (a bare `user_id` string is wrapped
270
+ * as `{ user_id }`); if the user is enrolled, POSTs a single exposure to
271
+ * `/collect`. No-op in test mode or when the user isn't enrolled.
272
+ */
273
+ logExposure(user: string | User, name: string): void;
274
+ /**
275
+ * Report a structured error into the errors primitive. Fire-and-forget —
276
+ * never blocks or throws into the request path. Spam-guarded by a 30s
277
+ * dedup window + per-process cap.
278
+ */
279
+ reportError(problem: unknown, consequence: Consequence, extras?: SeeExtras, kind?: SeeKind): void;
280
+ /**
281
+ * Evaluate all flags, configs, and experiments for a user against the locally
282
+ * cached blob (no network call). Applies ?se_ks_* / ?se_cf_* / ?se_exp_*
283
+ * overrides from the request URL when provided.
284
+ *
285
+ * Intended for SSR: call on the server, inject the result as
286
+ * `window.__SE_BOOTSTRAP` in the HTML, and the client SDK will read it
287
+ * synchronously without waiting for identify() to resolve.
288
+ */
289
+ evaluate(user: User, rawUrl?: string): BootstrapPayload;
290
+ getKillswitch(name: string, switchKey?: string): boolean;
291
+ }
292
+
293
+ /**
294
+ * OpenFeature **server** provider for Shipeasy.
295
+ *
296
+ * Lets apps standardized on the CNCF OpenFeature API plug Shipeasy in as the
297
+ * backing provider:
298
+ *
299
+ * ```ts
300
+ * import { OpenFeature } from "@openfeature/server-sdk";
301
+ * import { FlagsClient } from "@shipeasy/sdk/server";
302
+ * import { ShipeasyProvider } from "@shipeasy/sdk/openfeature-server";
303
+ *
304
+ * const client = new FlagsClient({ apiKey: process.env.SHIPEASY_SERVER_KEY! });
305
+ * await OpenFeature.setProviderAndWait(new ShipeasyProvider(client));
306
+ *
307
+ * const ofClient = OpenFeature.getClient();
308
+ * const on = await ofClient.getBooleanValue("new_checkout", false, { targetingKey: "u1" });
309
+ * ```
310
+ *
311
+ * Pure adapter over `FlagsClient` — no change to evaluation. `@openfeature/server-sdk`
312
+ * is an optional peer dependency; install it in the consuming app.
313
+ */
314
+
315
+ /**
316
+ * Shipeasy OpenFeature provider (server paradigm). Wraps a `FlagsClient`;
317
+ * evaluation is local against the cached blob, so resolution is effectively
318
+ * synchronous (the methods are `async` only to satisfy the server contract).
319
+ */
320
+ declare class ShipeasyProvider implements Provider {
321
+ private readonly client;
322
+ readonly metadata: {
323
+ readonly name: "shipeasy";
324
+ };
325
+ readonly runsOn: "server";
326
+ constructor(client: FlagsClient);
327
+ /** Fetch the rules blob once. The SDK fires `Ready` when this resolves. */
328
+ initialize(): Promise<void>;
329
+ onClose(): Promise<void>;
330
+ resolveBooleanEvaluation(flagKey: string, defaultValue: boolean, context: EvaluationContext, _logger?: Logger): Promise<ResolutionDetails<boolean>>;
331
+ resolveStringEvaluation(flagKey: string, defaultValue: string, _context: EvaluationContext, _logger?: Logger): Promise<ResolutionDetails<string>>;
332
+ resolveNumberEvaluation(flagKey: string, defaultValue: number, _context: EvaluationContext, _logger?: Logger): Promise<ResolutionDetails<number>>;
333
+ resolveObjectEvaluation<T extends JsonValue>(flagKey: string, defaultValue: T, _context: EvaluationContext, _logger?: Logger): Promise<ResolutionDetails<T>>;
334
+ /** OpenFeature `track()` → Shipeasy `track()`. No-ops without a targeting key. */
335
+ track(trackingEventName: string, context: EvaluationContext, details?: Record<string, unknown>): void;
336
+ }
337
+
338
+ export { ShipeasyProvider };