@shipeasy/sdk 5.1.0 → 5.3.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,332 @@
1
+ import { Provider, EvaluationContext, Logger, ResolutionDetails, JsonValue } from '@openfeature/web-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
+ declare global {
13
+ interface Window {
14
+ i18n?: {
15
+ t: (key: string, vars?: Record<string, string | number>) => string;
16
+ ready: (cb: () => void) => void;
17
+ on: (event: "update", cb: () => void) => () => void;
18
+ locale: string | null;
19
+ };
20
+ }
21
+ }
22
+ interface User {
23
+ user_id?: string;
24
+ [attr: string]: unknown;
25
+ }
26
+ interface ExperimentResult<P> {
27
+ inExperiment: boolean;
28
+ group: string;
29
+ params: P;
30
+ }
31
+ /** Options object form of `getExperiment` — the legacy `decode`/`variants`
32
+ * positional args plus per-call exposure control. */
33
+ interface GetExperimentOptions<P> {
34
+ /** Decode the raw stored params into the typed shape callers want. */
35
+ decode?: (raw: unknown) => P;
36
+ /** Variant-specific param overrides merged on top of group params. */
37
+ variants?: Record<string, Partial<P>>;
38
+ /**
39
+ * Override automatic exposure logging for this read. Defaults to the client's
40
+ * setting (`disableAutoExposure` flips it). `false` reads the variant without
41
+ * logging an exposure — pair with `logExposure(name)` at render time.
42
+ */
43
+ logExposure?: boolean;
44
+ }
45
+ /**
46
+ * Why a flag evaluated the way it did (LaunchDarkly variationDetail parity).
47
+ * Computed at the client boundary:
48
+ * - CLIENT_NOT_READY — no eval result yet (identify() hasn't resolved)
49
+ * - FLAG_NOT_FOUND — the gate name isn't present in the eval result
50
+ * - OFF — folded into DEFAULT here (the server pre-evaluates the
51
+ * gate's enabled/killed state into a plain boolean, so the browser can't
52
+ * distinguish a disabled gate from one a rule denied)
53
+ * - OVERRIDE — a local override or a ?se_gate_/?se_ks_ URL override
54
+ * decided the value
55
+ * - RULE_MATCH — the gate evaluated true
56
+ * - DEFAULT — the gate evaluated false
57
+ */
58
+ declare const FLAG_REASONS: readonly ["CLIENT_NOT_READY", "FLAG_NOT_FOUND", "OFF", "OVERRIDE", "RULE_MATCH", "DEFAULT"];
59
+ type FlagReason = (typeof FLAG_REASONS)[number];
60
+ interface FlagDetail {
61
+ value: boolean;
62
+ reason: FlagReason;
63
+ }
64
+ /** Options object form of `getConfig` — keeps the legacy `decode` callback and
65
+ * adds a `defaultValue` returned when the config key is absent. */
66
+ interface GetConfigOptions<T = unknown> {
67
+ /** Decode the raw stored value into the typed shape callers want. */
68
+ decode?: (raw: unknown) => T;
69
+ /** Returned when the config key is absent (not overridden, not in the eval result). */
70
+ defaultValue?: T;
71
+ }
72
+ interface EvalExpResult {
73
+ inExperiment: boolean;
74
+ group: string;
75
+ params: Record<string, unknown>;
76
+ }
77
+ interface EvalResponse {
78
+ flags: Record<string, boolean>;
79
+ configs: Record<string, unknown>;
80
+ experiments: Record<string, EvalExpResult>;
81
+ /**
82
+ * Killswitch state, flattened by the server. A boolean means the killswitch
83
+ * is whole-killed; an object means it's not whole-killed and carries per-
84
+ * switch booleans.
85
+ */
86
+ killswitches?: Record<string, boolean | Record<string, boolean>>;
87
+ /**
88
+ * Newly-assigned sticky entries (doc 20 §2). The worker returns these so the
89
+ * browser can merge them into the `__se_sticky` cookie. Present only when
90
+ * sticky bucketing is on and at least one assignment was made/refreshed.
91
+ */
92
+ sticky?: Record<string, StickyCookieEntry>;
93
+ }
94
+ /** One persisted sticky assignment: group + 8-char salt prefix (reshuffle key). */
95
+ interface StickyCookieEntry {
96
+ g: string;
97
+ s: string;
98
+ }
99
+ interface AutoCollectGroups {
100
+ vitals: boolean;
101
+ errors: boolean;
102
+ engagement: boolean;
103
+ }
104
+ type FlagsClientBrowserEnv = "dev" | "staging" | "prod";
105
+ interface FlagsClientBrowserOptions {
106
+ sdkKey: string;
107
+ baseUrl?: string;
108
+ autoGuardrails?: boolean;
109
+ /**
110
+ * Per-group enablement for auto-collected metrics. When set, overrides the
111
+ * blanket `autoGuardrails` flag for the specific groups listed. Any group
112
+ * not present in the object falls back to `autoGuardrails` (defaulting to
113
+ * true when `autoGuardrails` is true).
114
+ */
115
+ autoGuardrailGroups?: Partial<AutoCollectGroups>;
116
+ /**
117
+ * Emit `__auto_*` metric events for ALL visitors, not just experiment
118
+ * participants. Default false: auto-metrics are gated on the visitor having
119
+ * seen ≥1 experiment exposure (the only data the analysis pipeline reads;
120
+ * ungated emission is pure AE write cost at scale — see cost.md).
121
+ */
122
+ autoCollectAlways?: boolean;
123
+ /** Which published env to read values from. Defaults to "prod". */
124
+ env?: FlagsClientBrowserEnv;
125
+ /**
126
+ * Per-evaluation usage telemetry. ON by default — each getFlag/getConfig/
127
+ * getExperiment/getKillswitch call fires one fire-and-forget sendBeacon so
128
+ * usage is counted by Cloudflare's native per-path analytics. Pass `true` to
129
+ * disable entirely.
130
+ */
131
+ disableTelemetry?: boolean;
132
+ /** Override the telemetry beacon host. Defaults to {@link DEFAULT_TELEMETRY_URL}. */
133
+ telemetryUrl?: string;
134
+ /**
135
+ * Suppress automatic exposure logging in `getExperiment` (Statsig's
136
+ * `disableExposureLogging`). Default false — reading an enrolled variant
137
+ * auto-logs a deduped exposure. When true, no exposure fires unless you call
138
+ * `logExposure(name)` yourself, or pass `{ logExposure: true }` per call.
139
+ */
140
+ disableAutoExposure?: boolean;
141
+ /**
142
+ * Attribute names usable for targeting but never persisted in analytics
143
+ * (LD/Statsig `privateAttributes`). They are sent to `/sdk/evaluate` under
144
+ * `private_attributes` so the edge can evaluate with them (unavoidable —
145
+ * the edge evaluates), but the worker never stores them, and the listed keys
146
+ * are stripped from any `track(props)` payload.
147
+ */
148
+ privateAttributes?: string[];
149
+ /**
150
+ * Sticky bucketing (doc 20 §2). ON by default in the browser: a unit's
151
+ * first-assigned variant is locked in the `__se_sticky` cookie so changing an
152
+ * experiment's allocation % or group weights never silently re-buckets
153
+ * enrolled users. Changing the experiment salt is the deliberate reshuffle
154
+ * lever. Pass `false` to opt out (pure deterministic eval).
155
+ */
156
+ stickyBucketing?: boolean;
157
+ /**
158
+ * Test mode — no network at all. identify()/init are no-ops (never call
159
+ * /sdk/evaluate), track() is a no-op, telemetry is forced off, and the client
160
+ * starts "ready" with an empty eval result. Prefer the
161
+ * {@link FlagsClientBrowser.forTesting} factory over passing this directly.
162
+ */
163
+ testMode?: boolean;
164
+ }
165
+ declare class FlagsClientBrowser {
166
+ private readonly sdkKey;
167
+ private readonly baseUrl;
168
+ private readonly autoGuardrails;
169
+ private readonly autoGuardrailGroups;
170
+ private readonly autoCollectAlways;
171
+ private readonly disableAutoExposure;
172
+ private readonly privateAttributes;
173
+ private readonly stickyBucketing;
174
+ private readonly env;
175
+ private evalResult;
176
+ private anonId;
177
+ private userId;
178
+ private buffer;
179
+ private telemetry;
180
+ private seeLimiter;
181
+ private guardrailsInstalled;
182
+ private listeners;
183
+ private overrideListenerInstalled;
184
+ private identifySeq;
185
+ private readonly testMode;
186
+ private readonly flagOverrides;
187
+ private readonly configOverrides;
188
+ private readonly experimentOverrides;
189
+ private onOverrideChange;
190
+ constructor(opts: FlagsClientBrowserOptions);
191
+ /**
192
+ * Build a no-network, immediately-usable browser client for tests
193
+ * (Statsig-style). identify() is a no-op (never calls /sdk/evaluate), track()
194
+ * is a no-op, telemetry is disabled, and the client is already ready — seed
195
+ * every entity with overrideFlag/overrideConfig/overrideExperiment. No SDK
196
+ * key required.
197
+ *
198
+ * ```ts
199
+ * const client = FlagsClientBrowser.forTesting();
200
+ * client.overrideFlag("new_checkout", true);
201
+ * client.getFlag("new_checkout"); // true
202
+ * ```
203
+ */
204
+ static forTesting(opts?: Partial<FlagsClientBrowserOptions>): FlagsClientBrowser;
205
+ identify(user: User): Promise<void>;
206
+ /**
207
+ * Report a structured error into the errors primitive. Flushes immediately
208
+ * (beacon-first) — error occurrences are near-real-time, never queued behind
209
+ * the 5s metric batch. Spam-guarded by a 30s dedup window + per-session cap.
210
+ */
211
+ reportError(problem: unknown, consequence: Consequence, extras?: SeeExtras, kind?: SeeKind, correlationId?: string): void;
212
+ get ready(): boolean;
213
+ private notify;
214
+ initFromBootstrap(data: EvalResponse): void;
215
+ /** Force `getFlag(name)` to return `value`, ignoring URL overrides + eval. */
216
+ overrideFlag(name: string, value: boolean): void;
217
+ /** Force `getConfig(name)` to return `value`, ignoring URL overrides + eval. */
218
+ overrideConfig(name: string, value: unknown): void;
219
+ /**
220
+ * Force `getExperiment(name, …)` to return `{ inExperiment: true, group, params }`,
221
+ * ignoring URL overrides, the eval result, and exposure logging.
222
+ */
223
+ overrideExperiment(name: string, group: string, params: Record<string, unknown>): void;
224
+ /** Remove every programmatic override set via the override* methods. */
225
+ clearOverrides(): void;
226
+ /**
227
+ * Evaluate a gate and report WHY (LaunchDarkly variationDetail parity). A
228
+ * local override OR a ?se_gate_/?se_ks_ URL override short-circuits BEFORE
229
+ * telemetry; otherwise exactly one "gate" beacon is emitted. The server
230
+ * pre-evaluates a gate's enabled/killed state into a plain boolean, so OFF
231
+ * folds into DEFAULT here (the browser can't tell "disabled" from "rule
232
+ * denied").
233
+ */
234
+ getFlagDetail(name: string): FlagDetail;
235
+ /**
236
+ * Read a feature gate. Returns `defaultValue` ONLY when the gate cannot be
237
+ * evaluated (not ready or flag not found) — never for a gate that legitimately
238
+ * evaluates to false. Plain `getFlag(name)` keeps returning false for a
239
+ * missing flag.
240
+ */
241
+ getFlag(name: string, defaultValue?: boolean): boolean;
242
+ getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
243
+ getConfig<T = unknown>(name: string, opts: GetConfigOptions<T>): T;
244
+ getExperiment<P extends Record<string, unknown>>(name: string, defaultParams: P, decode?: (raw: unknown) => P, variants?: Record<string, Partial<P>>): ExperimentResult<P>;
245
+ getExperiment<P extends Record<string, unknown>>(name: string, defaultParams: P, opts: GetExperimentOptions<P>): ExperimentResult<P>;
246
+ /**
247
+ * Manually log an exposure for an enrolled experiment (Statsig's
248
+ * `manuallyLogExposure`). Reads the cached eval result; if the visitor is in
249
+ * the experiment, pushes the session-deduped exposure. Pair this with the
250
+ * render of the treatment when reading with `{ logExposure: false }` (or
251
+ * `disableAutoExposure: true`). No-op if the visitor isn't enrolled.
252
+ */
253
+ logExposure(name: string): void;
254
+ /**
255
+ * Subscribe to state changes — fires after identify() completes and on
256
+ * `se:override:change` events from the devtools overlay. Returns an
257
+ * unsubscribe function. Used by framework adapters to trigger re-renders.
258
+ */
259
+ subscribe(listener: () => void): () => void;
260
+ /**
261
+ * Publishes the SDK to `window.__shipeasy` so the devtools overlay can read
262
+ * current values. Idempotent. Returns the bridge object for tests.
263
+ */
264
+ installBridge(): ShipeasySdkBridge | null;
265
+ track(eventName: string, props?: Record<string, unknown>): void;
266
+ /** Drop caller-marked private attributes from an outbound props bag. */
267
+ private stripPrivate;
268
+ /**
269
+ * Read a killswitch from the server's evaluated state. Without `switchKey`,
270
+ * returns true when the killswitch is whole-killed. With `switchKey`, returns
271
+ * the per-switch state. Returns false for unknown killswitches / switches.
272
+ */
273
+ getKillswitch(name: string, switchKey?: string): boolean;
274
+ flush(): Promise<void>;
275
+ destroy(): void;
276
+ }
277
+ /** Bridge written to window.__shipeasy — mirrors @shipeasy/devtools' contract. */
278
+ interface ShipeasySdkBridge {
279
+ getFlag(name: string): boolean;
280
+ getExperiment(name: string): {
281
+ inExperiment: boolean;
282
+ group: string;
283
+ } | undefined;
284
+ getConfig(name: string): unknown;
285
+ }
286
+
287
+ /**
288
+ * OpenFeature **web** provider for Shipeasy.
289
+ *
290
+ * ```ts
291
+ * import { OpenFeature } from "@openfeature/web-sdk";
292
+ * import { FlagsClientBrowser } from "@shipeasy/sdk/client";
293
+ * import { ShipeasyProvider } from "@shipeasy/sdk/openfeature-web";
294
+ *
295
+ * const client = new FlagsClientBrowser({ sdkKey: NEXT_PUBLIC_SHIPEASY_CLIENT_KEY });
296
+ * await OpenFeature.setContext({ targetingKey: "u1", plan: "pro" });
297
+ * await OpenFeature.setProviderAndWait(new ShipeasyProvider(client));
298
+ *
299
+ * const on = OpenFeature.getClient().getBooleanValue("new_checkout", false);
300
+ * ```
301
+ *
302
+ * The browser evaluates at the edge, so the provider maps OpenFeature's static
303
+ * context onto `client.identify()` (via `initialize` + `onContextChange`) and
304
+ * reads the cached eval result synchronously in the resolve methods.
305
+ * `@openfeature/web-sdk` is an optional peer dependency.
306
+ */
307
+
308
+ /**
309
+ * Shipeasy OpenFeature provider (client/web paradigm). Wraps a
310
+ * `FlagsClientBrowser`. Context changes are reconciled through `identify()`;
311
+ * flag reads come from the cached eval result.
312
+ */
313
+ declare class ShipeasyProvider implements Provider {
314
+ private readonly client;
315
+ readonly metadata: {
316
+ readonly name: "shipeasy";
317
+ };
318
+ readonly runsOn: "client";
319
+ constructor(client: FlagsClientBrowser);
320
+ /** Identify with the initial static context so the first read is warm. */
321
+ initialize(context?: EvaluationContext): Promise<void>;
322
+ /** Re-identify when the application author changes the static context. */
323
+ onContextChange(_old: EvaluationContext, next: EvaluationContext): Promise<void>;
324
+ resolveBooleanEvaluation(flagKey: string, defaultValue: boolean, _context: EvaluationContext, _logger?: Logger): ResolutionDetails<boolean>;
325
+ resolveStringEvaluation(flagKey: string, defaultValue: string, _context: EvaluationContext, _logger?: Logger): ResolutionDetails<string>;
326
+ resolveNumberEvaluation(flagKey: string, defaultValue: number, _context: EvaluationContext, _logger?: Logger): ResolutionDetails<number>;
327
+ resolveObjectEvaluation<T extends JsonValue>(flagKey: string, defaultValue: T, _context: EvaluationContext, _logger?: Logger): ResolutionDetails<T>;
328
+ /** OpenFeature `track()` → Shipeasy `track()` (uses the identified user/anon). */
329
+ track(trackingEventName: string, _context: EvaluationContext, details?: Record<string, unknown>): void;
330
+ }
331
+
332
+ export { ShipeasyProvider };
@@ -0,0 +1,120 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/openfeature-web/index.ts
21
+ var openfeature_web_exports = {};
22
+ __export(openfeature_web_exports, {
23
+ ShipeasyProvider: () => ShipeasyProvider
24
+ });
25
+ module.exports = __toCommonJS(openfeature_web_exports);
26
+ var import_web_sdk = require("@openfeature/web-sdk");
27
+
28
+ // src/openfeature/shared.ts
29
+ var REASON_MAP = {
30
+ RULE_MATCH: { reason: "TARGETING_MATCH" },
31
+ DEFAULT: { reason: "DEFAULT" },
32
+ OFF: { reason: "DISABLED" },
33
+ OVERRIDE: { reason: "STATIC" },
34
+ FLAG_NOT_FOUND: { reason: "ERROR", errorCode: "FLAG_NOT_FOUND" },
35
+ CLIENT_NOT_READY: { reason: "ERROR", errorCode: "PROVIDER_NOT_READY" }
36
+ };
37
+ function mapFlagReason(reason) {
38
+ return REASON_MAP[reason] ?? { reason: "UNKNOWN" };
39
+ }
40
+ function toUser(ctx) {
41
+ if (!ctx) return {};
42
+ const { targetingKey, ...rest } = ctx;
43
+ const user = { ...rest };
44
+ if (typeof targetingKey === "string" && targetingKey.length > 0) {
45
+ user.user_id = targetingKey;
46
+ }
47
+ return user;
48
+ }
49
+ function resolveConfigValue(raw, type) {
50
+ if (raw === void 0) return { kind: "default" };
51
+ const ok = type === "object" ? typeof raw === "object" && raw !== null : typeof raw === type;
52
+ if (!ok) {
53
+ return {
54
+ kind: "type_mismatch",
55
+ message: `flag value ${JSON.stringify(raw)} is not of type ${type}`
56
+ };
57
+ }
58
+ return { kind: "match", value: raw };
59
+ }
60
+
61
+ // src/openfeature-web/index.ts
62
+ var ERROR_CODE = {
63
+ FLAG_NOT_FOUND: import_web_sdk.ErrorCode.FLAG_NOT_FOUND,
64
+ PROVIDER_NOT_READY: import_web_sdk.ErrorCode.PROVIDER_NOT_READY,
65
+ TYPE_MISMATCH: import_web_sdk.ErrorCode.TYPE_MISMATCH
66
+ };
67
+ function resolveConfig(raw, defaultValue, type) {
68
+ const r = resolveConfigValue(raw, type);
69
+ if (r.kind === "default") return { value: defaultValue, reason: "DEFAULT" };
70
+ if (r.kind === "type_mismatch") {
71
+ return {
72
+ value: defaultValue,
73
+ reason: "ERROR",
74
+ errorCode: ERROR_CODE.TYPE_MISMATCH,
75
+ errorMessage: r.message
76
+ };
77
+ }
78
+ return { value: r.value, reason: "TARGETING_MATCH" };
79
+ }
80
+ var ShipeasyProvider = class {
81
+ constructor(client) {
82
+ this.client = client;
83
+ }
84
+ client;
85
+ metadata = { name: "shipeasy" };
86
+ runsOn = "client";
87
+ /** Identify with the initial static context so the first read is warm. */
88
+ async initialize(context) {
89
+ await this.client.identify(toUser(context));
90
+ }
91
+ /** Re-identify when the application author changes the static context. */
92
+ async onContextChange(_old, next) {
93
+ await this.client.identify(toUser(next));
94
+ }
95
+ resolveBooleanEvaluation(flagKey, defaultValue, _context, _logger) {
96
+ const detail = this.client.getFlagDetail(flagKey);
97
+ const m = mapFlagReason(detail.reason);
98
+ if (m.errorCode) {
99
+ return { value: defaultValue, reason: m.reason, errorCode: ERROR_CODE[m.errorCode] };
100
+ }
101
+ return { value: detail.value, reason: m.reason };
102
+ }
103
+ resolveStringEvaluation(flagKey, defaultValue, _context, _logger) {
104
+ return resolveConfig(this.client.getConfig(flagKey), defaultValue, "string");
105
+ }
106
+ resolveNumberEvaluation(flagKey, defaultValue, _context, _logger) {
107
+ return resolveConfig(this.client.getConfig(flagKey), defaultValue, "number");
108
+ }
109
+ resolveObjectEvaluation(flagKey, defaultValue, _context, _logger) {
110
+ return resolveConfig(this.client.getConfig(flagKey), defaultValue, "object");
111
+ }
112
+ /** OpenFeature `track()` → Shipeasy `track()` (uses the identified user/anon). */
113
+ track(trackingEventName, _context, details) {
114
+ this.client.track(trackingEventName, details);
115
+ }
116
+ };
117
+ // Annotate the CommonJS export names for ESM import in node:
118
+ 0 && (module.exports = {
119
+ ShipeasyProvider
120
+ });
@@ -0,0 +1,95 @@
1
+ // src/openfeature-web/index.ts
2
+ import { ErrorCode } from "@openfeature/web-sdk";
3
+
4
+ // src/openfeature/shared.ts
5
+ var REASON_MAP = {
6
+ RULE_MATCH: { reason: "TARGETING_MATCH" },
7
+ DEFAULT: { reason: "DEFAULT" },
8
+ OFF: { reason: "DISABLED" },
9
+ OVERRIDE: { reason: "STATIC" },
10
+ FLAG_NOT_FOUND: { reason: "ERROR", errorCode: "FLAG_NOT_FOUND" },
11
+ CLIENT_NOT_READY: { reason: "ERROR", errorCode: "PROVIDER_NOT_READY" }
12
+ };
13
+ function mapFlagReason(reason) {
14
+ return REASON_MAP[reason] ?? { reason: "UNKNOWN" };
15
+ }
16
+ function toUser(ctx) {
17
+ if (!ctx) return {};
18
+ const { targetingKey, ...rest } = ctx;
19
+ const user = { ...rest };
20
+ if (typeof targetingKey === "string" && targetingKey.length > 0) {
21
+ user.user_id = targetingKey;
22
+ }
23
+ return user;
24
+ }
25
+ function resolveConfigValue(raw, type) {
26
+ if (raw === void 0) return { kind: "default" };
27
+ const ok = type === "object" ? typeof raw === "object" && raw !== null : typeof raw === type;
28
+ if (!ok) {
29
+ return {
30
+ kind: "type_mismatch",
31
+ message: `flag value ${JSON.stringify(raw)} is not of type ${type}`
32
+ };
33
+ }
34
+ return { kind: "match", value: raw };
35
+ }
36
+
37
+ // src/openfeature-web/index.ts
38
+ var ERROR_CODE = {
39
+ FLAG_NOT_FOUND: ErrorCode.FLAG_NOT_FOUND,
40
+ PROVIDER_NOT_READY: ErrorCode.PROVIDER_NOT_READY,
41
+ TYPE_MISMATCH: ErrorCode.TYPE_MISMATCH
42
+ };
43
+ function resolveConfig(raw, defaultValue, type) {
44
+ const r = resolveConfigValue(raw, type);
45
+ if (r.kind === "default") return { value: defaultValue, reason: "DEFAULT" };
46
+ if (r.kind === "type_mismatch") {
47
+ return {
48
+ value: defaultValue,
49
+ reason: "ERROR",
50
+ errorCode: ERROR_CODE.TYPE_MISMATCH,
51
+ errorMessage: r.message
52
+ };
53
+ }
54
+ return { value: r.value, reason: "TARGETING_MATCH" };
55
+ }
56
+ var ShipeasyProvider = class {
57
+ constructor(client) {
58
+ this.client = client;
59
+ }
60
+ client;
61
+ metadata = { name: "shipeasy" };
62
+ runsOn = "client";
63
+ /** Identify with the initial static context so the first read is warm. */
64
+ async initialize(context) {
65
+ await this.client.identify(toUser(context));
66
+ }
67
+ /** Re-identify when the application author changes the static context. */
68
+ async onContextChange(_old, next) {
69
+ await this.client.identify(toUser(next));
70
+ }
71
+ resolveBooleanEvaluation(flagKey, defaultValue, _context, _logger) {
72
+ const detail = this.client.getFlagDetail(flagKey);
73
+ const m = mapFlagReason(detail.reason);
74
+ if (m.errorCode) {
75
+ return { value: defaultValue, reason: m.reason, errorCode: ERROR_CODE[m.errorCode] };
76
+ }
77
+ return { value: detail.value, reason: m.reason };
78
+ }
79
+ resolveStringEvaluation(flagKey, defaultValue, _context, _logger) {
80
+ return resolveConfig(this.client.getConfig(flagKey), defaultValue, "string");
81
+ }
82
+ resolveNumberEvaluation(flagKey, defaultValue, _context, _logger) {
83
+ return resolveConfig(this.client.getConfig(flagKey), defaultValue, "number");
84
+ }
85
+ resolveObjectEvaluation(flagKey, defaultValue, _context, _logger) {
86
+ return resolveConfig(this.client.getConfig(flagKey), defaultValue, "object");
87
+ }
88
+ /** OpenFeature `track()` → Shipeasy `track()` (uses the identified user/anon). */
89
+ track(trackingEventName, _context, details) {
90
+ this.client.track(trackingEventName, details);
91
+ }
92
+ };
93
+ export {
94
+ ShipeasyProvider
95
+ };
@@ -170,7 +170,27 @@ interface Experiment {
170
170
  salt: string;
171
171
  groups: ExperimentGroup[];
172
172
  status: "draft" | "running" | "stopped" | "archived";
173
+ /** Attribute to bucket on (e.g. company_id); defaults to user_id/anonymous_id. */
174
+ bucketBy?: string | null;
173
175
  }
176
+ /** One persisted sticky assignment: group + 8-char salt prefix (reshuffle key). */
177
+ interface StickyEntry {
178
+ g: string;
179
+ s: string;
180
+ }
181
+ /**
182
+ * Pluggable sticky-bucketing store for the server (doc 20 §2). Keyed by the
183
+ * bucketing unit; the value is that unit's per-experiment assignments. Absent
184
+ * from {@link FlagsClientOptions} ⇒ today's deterministic behaviour. Use
185
+ * {@link createInMemoryStickyStore} or a cookie-bridge built from request
186
+ * cookies.
187
+ */
188
+ interface StickyBucketStore {
189
+ get(unit: string): Record<string, StickyEntry> | undefined;
190
+ set(unit: string, exp: string, entry: StickyEntry): void;
191
+ }
192
+ /** A process-local sticky store (Map-backed). Handy for tests + single-process servers. */
193
+ declare function createInMemoryStickyStore(seed?: Record<string, Record<string, StickyEntry>>): StickyBucketStore;
174
194
  interface Universe {
175
195
  holdout_range: [number, number] | null;
176
196
  }
@@ -223,6 +243,21 @@ interface FlagsClientOptions {
223
243
  disableTelemetry?: boolean;
224
244
  /** Override the telemetry beacon host. Defaults to {@link DEFAULT_TELEMETRY_URL}. */
225
245
  telemetryUrl?: string;
246
+ /**
247
+ * Attribute names usable for targeting but never persisted in analytics
248
+ * (LD/Statsig `privateAttributes`). The server evaluates locally so private
249
+ * attrs never leave for evaluation at all; the only egress is `/collect`, and
250
+ * the listed keys are stripped from every outbound `track()` payload.
251
+ */
252
+ privateAttributes?: string[];
253
+ /**
254
+ * Sticky-bucketing store (doc 20 §2). When provided, `getExperiment` locks a
255
+ * unit to its first-assigned variant — changing allocation % or weights won't
256
+ * re-bucket enrolled units (changing the experiment salt is the reshuffle
257
+ * lever). Absent ⇒ deterministic (fully backward compatible). Built-ins:
258
+ * {@link createInMemoryStickyStore}, or a cookie-bridge over `__se_sticky`.
259
+ */
260
+ stickyStore?: StickyBucketStore;
226
261
  /**
227
262
  * Test mode — no network at all. init()/initOnce() are no-ops (never fetch),
228
263
  * track() is a no-op, telemetry is forced off, and the client starts
@@ -235,6 +270,8 @@ declare class FlagsClient {
235
270
  private readonly apiKey;
236
271
  private readonly baseUrl;
237
272
  private readonly env;
273
+ private readonly privateAttributes;
274
+ private readonly stickyStore;
238
275
  private readonly telemetry;
239
276
  private readonly seeLimiter;
240
277
  private flagsBlob;
@@ -328,7 +365,18 @@ declare class FlagsClient {
328
365
  getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
329
366
  getConfig<T = unknown>(name: string, opts: GetConfigOptions<T>): T;
330
367
  getExperiment<P extends Record<string, unknown>>(name: string, user: User, defaultParams: P, decode?: (raw: unknown) => P): ExperimentResult<P>;
368
+ /** Drop caller-marked private attributes from an outbound props bag. */
369
+ private stripPrivate;
331
370
  track(userId: string, eventName: string, props?: Record<string, unknown>): void;
371
+ /**
372
+ * Emit an exposure event for an experiment at the server-side decision point
373
+ * (parity with the browser's auto-exposure). The server is stateless and
374
+ * never auto-logs, so call this when you actually present the treatment.
375
+ * Re-evaluates the experiment for `user` (a bare `user_id` string is wrapped
376
+ * as `{ user_id }`); if the user is enrolled, POSTs a single exposure to
377
+ * `/collect`. No-op in test mode or when the user isn't enrolled.
378
+ */
379
+ logExposure(user: string | User, name: string): void;
332
380
  /**
333
381
  * Report a structured error into the errors primitive. Fire-and-forget —
334
382
  * never blocks or throws into the request path. Spam-guarded by a 30s
@@ -418,6 +466,12 @@ interface ShipeasyServerConfig {
418
466
  * that evaluate many flags per request. See {@link FlagsClientOptions.disableTelemetry}.
419
467
  */
420
468
  disableTelemetry?: boolean;
469
+ /**
470
+ * Attribute names usable for targeting but never persisted in analytics
471
+ * (LD/Statsig `privateAttributes`). Stripped from every outbound `track()`
472
+ * payload. See {@link FlagsClientOptions.privateAttributes}.
473
+ */
474
+ privateAttributes?: string[];
421
475
  }
422
476
  interface ShipeasyServerHandle {
423
477
  flags: Record<string, boolean>;
@@ -485,6 +539,9 @@ declare const flags: {
485
539
  */
486
540
  ks(name: string, switchKey?: string): boolean;
487
541
  track(userId: string, eventName: string, props?: Record<string, unknown>): void;
542
+ /** Emit an exposure for an enrolled experiment at the decision point. See
543
+ * {@link FlagsClient.logExposure}. No-op before configure(). */
544
+ logExposure(user: string | User, name: string): void;
488
545
  /**
489
546
  * Evaluate all flags / configs / experiments for a user against the locally
490
547
  * cached blob. Pass the request URL to apply ?se_ks_* / ?se_cf_* / ?se_exp_*
@@ -543,4 +600,4 @@ interface SeeApi {
543
600
  */
544
601
  declare const see: SeeApi;
545
602
 
546
- export { ANON_ID_COOKIE, type BootstrapHtmlOptions, 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 SeeApi, type SeeChain, type SeeControlFlowChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyServerConfig, type ShipeasyServerHandle, type User, type Violation, _murmur3ForTests, _resetShipeasyServerForTests, configureShipeasyServer, fetchLabelsForSSR, flags, getBootstrapHtml, getShipeasyServerClient, i18n, isExpected, see, seeContext, shipeasy, version };
603
+ export { ANON_ID_COOKIE, type BootstrapHtmlOptions, 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 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, getBootstrapHtml, getShipeasyServerClient, i18n, isExpected, see, seeContext, shipeasy, version };