@fireweaveai/web-sdk-cjs 0.0.0-reserve.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.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +52 -0
  3. package/dist/application/client.d.ts +104 -0
  4. package/dist/application/client.js +147 -0
  5. package/dist/application/mode.d.ts +99 -0
  6. package/dist/application/mode.js +101 -0
  7. package/dist/application/ports.d.ts +101 -0
  8. package/dist/application/ports.js +2 -0
  9. package/dist/application/runtime.d.ts +70 -0
  10. package/dist/application/runtime.js +348 -0
  11. package/dist/build/node/define.d.ts +14 -0
  12. package/dist/build/node/define.js +38 -0
  13. package/dist/build/node/shared.d.ts +19 -0
  14. package/dist/build/node/shared.js +39 -0
  15. package/dist/build/node/vite.d.ts +53 -0
  16. package/dist/build/node/vite.js +106 -0
  17. package/dist/build/src/start/build-info.d.ts +2 -0
  18. package/dist/build/src/start/build-info.js +9 -0
  19. package/dist/build/src/start/names.d.ts +38 -0
  20. package/dist/build/src/start/names.js +41 -0
  21. package/dist/build/src/start/policy.d.ts +70 -0
  22. package/dist/build/src/start/policy.js +128 -0
  23. package/dist/domain/context.d.ts +51 -0
  24. package/dist/domain/context.js +72 -0
  25. package/dist/domain/errors.d.ts +36 -0
  26. package/dist/domain/errors.js +97 -0
  27. package/dist/domain/target.d.ts +2 -0
  28. package/dist/domain/target.js +2 -0
  29. package/dist/domain/types.d.ts +89 -0
  30. package/dist/domain/types.js +10 -0
  31. package/dist/domain/validation.d.ts +128 -0
  32. package/dist/domain/validation.js +358 -0
  33. package/dist/index.d.ts +50 -0
  34. package/dist/index.js +66 -0
  35. package/dist/infrastructure/adapters/inmemory.d.ts +50 -0
  36. package/dist/infrastructure/adapters/inmemory.js +94 -0
  37. package/dist/infrastructure/adapters/local.d.ts +79 -0
  38. package/dist/infrastructure/adapters/local.js +98 -0
  39. package/dist/infrastructure/adapters/remote.d.ts +44 -0
  40. package/dist/infrastructure/adapters/remote.js +329 -0
  41. package/dist/infrastructure/hosts.d.ts +10 -0
  42. package/dist/infrastructure/hosts.js +84 -0
  43. package/dist/start/build-info.d.ts +2 -0
  44. package/dist/start/build-info.js +9 -0
  45. package/dist/start/control-points.d.ts +24 -0
  46. package/dist/start/control-points.js +61 -0
  47. package/dist/start/fw.d.ts +42 -0
  48. package/dist/start/fw.js +137 -0
  49. package/dist/start/identity.d.ts +19 -0
  50. package/dist/start/identity.js +93 -0
  51. package/dist/start/index.d.ts +34 -0
  52. package/dist/start/index.js +38 -0
  53. package/dist/start/names.d.ts +38 -0
  54. package/dist/start/names.js +41 -0
  55. package/dist/start/policy.d.ts +70 -0
  56. package/dist/start/policy.js +128 -0
  57. package/dist/start/state.d.ts +103 -0
  58. package/dist/start/state.js +375 -0
  59. package/package.json +68 -0
@@ -0,0 +1,329 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FireweaveRemoteWebAdapter = void 0;
4
+ /**
5
+ * Fireweave remote adapter for browsers — the production path.
6
+ *
7
+ * Speaks only the vendor-neutral Fireweave remote protocol to fw-server:
8
+ * POST /v1/control-points/evaluate (batch — one call per context, not per key)
9
+ * POST /v1/capture
10
+ * POST /v1/targets/register
11
+ *
12
+ * Auth: `Authorization: Bearer <project key>`. Never a vendor key: the
13
+ * constructor rejects `phc_`/`phs_`/`phx_` shapes outright (ADR-0009).
14
+ *
15
+ * Credentials are EXPLICIT constructor options. This package never reads
16
+ * `import.meta.env`, `process.env`, or any other ambient source — the embedding
17
+ * app decides what goes into its bundle, and the SDK never picks something up
18
+ * on its own.
19
+ *
20
+ * See spec/remote-protocol.md § Browser clients.
21
+ */
22
+ const errors_js_1 = require("../../domain/errors.js");
23
+ const hosts_js_1 = require("../hosts.js");
24
+ const validation_js_1 = require("../../domain/validation.js");
25
+ const EVALUATE_PATH = '/v1/control-points/evaluate';
26
+ const CAPTURE_PATH = '/v1/capture';
27
+ const REGISTER_TARGET_PATH = '/v1/targets/register';
28
+ const DEFAULT_REQUEST_TIMEOUT_MS = 3_000;
29
+ const ALLOWED_REASONS = new Set([
30
+ 'STATIC',
31
+ 'DEFAULT',
32
+ 'TARGETING_MATCH',
33
+ 'SPLIT',
34
+ 'CACHED',
35
+ 'STALE',
36
+ 'DISABLED',
37
+ 'UNKNOWN',
38
+ 'ERROR',
39
+ ]);
40
+ function toReason(reason) {
41
+ return ALLOWED_REASONS.has(reason) ? reason : undefined;
42
+ }
43
+ function mapHttpStatus(status) {
44
+ if (status === 401)
45
+ return new errors_js_1.FireweaveError('Authentication');
46
+ if (status === 403)
47
+ return new errors_js_1.FireweaveError('Authorization');
48
+ if (status === 429)
49
+ return new errors_js_1.FireweaveError('RateLimited');
50
+ return new errors_js_1.FireweaveError('BackendUnavailable');
51
+ }
52
+ class FireweaveRemoteWebAdapter {
53
+ name = 'fireweave';
54
+ apiUrl;
55
+ apiKey;
56
+ allowedHosts;
57
+ requestTimeoutMs;
58
+ fetchImpl;
59
+ pending = [];
60
+ ready = false;
61
+ closed = false;
62
+ constructor(options) {
63
+ // Validate at construction, not at first use: a misconfigured key should
64
+ // fail where the developer can see it, not on a user's first evaluation.
65
+ (0, hosts_js_1.assertNotSecretKey)(options.apiKey);
66
+ this.apiUrl = options.apiUrl.trim().replace(/\/+$/, '');
67
+ this.apiKey = options.apiKey.trim();
68
+ this.allowedHosts = options.allowedHosts;
69
+ this.requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
70
+ this.fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
71
+ }
72
+ async initialize(_signal) {
73
+ if (this.closed)
74
+ throw new errors_js_1.FireweaveError('AlreadyClosed');
75
+ if (this.apiUrl.length === 0)
76
+ throw new errors_js_1.FireweaveError('Configuration');
77
+ (0, hosts_js_1.assertHostAllowed)(this.apiUrl, this.allowedHosts);
78
+ this.ready = true;
79
+ }
80
+ async prefetch(context, options) {
81
+ if (this.closed)
82
+ throw new errors_js_1.FireweaveError('AlreadyClosed');
83
+ if (!this.ready)
84
+ throw new errors_js_1.FireweaveError('NotReady');
85
+ const targetingKeyResult = (0, validation_js_1.validateTargetingKey)(context.targetingKey, true);
86
+ if (!targetingKeyResult.ok)
87
+ throw targetingKeyResult.error;
88
+ const targetingKey = targetingKeyResult.value ?? '';
89
+ // `$`-prefixed and `fireweave.`-prefixed attributes are backend directives,
90
+ // not person properties, and groups travel in their own fields.
91
+ const attributes = {};
92
+ for (const [k, v] of Object.entries(context.attributes)) {
93
+ if (k === 'groups' || k === 'groupProperties')
94
+ continue;
95
+ if (k.startsWith('$') || k.startsWith('fireweave.'))
96
+ continue;
97
+ attributes[k] = v;
98
+ }
99
+ const body = { targetingKey };
100
+ if (Object.keys(attributes).length > 0)
101
+ body['attributes'] = attributes;
102
+ if (context.groups !== undefined)
103
+ body['groups'] = context.groups;
104
+ if (context.groupProperties !== undefined)
105
+ body['groupProperties'] = context.groupProperties;
106
+ if (options?.controlPointKeys !== undefined && options.controlPointKeys.length > 0) {
107
+ body['controlPointKeys'] = options.controlPointKeys;
108
+ }
109
+ const response = await this.requestJson(EVALUATE_PATH, body, options?.signal);
110
+ const out = new Map();
111
+ for (const item of response.decisions ?? []) {
112
+ out.set(item.controlPointKey, this.toResolution(item, response.quotaLimited === true));
113
+ }
114
+ return out;
115
+ }
116
+ async registerTarget(targetingKey, options = {}) {
117
+ if (this.closed)
118
+ return { ok: false, error: new errors_js_1.FireweaveError('AlreadyClosed') };
119
+ if (!this.ready)
120
+ return { ok: false, error: new errors_js_1.FireweaveError('NotReady') };
121
+ const targetingKeyResult = (0, validation_js_1.validateTargetingKey)(targetingKey.trim() === '' ? undefined : targetingKey, true);
122
+ if (!targetingKeyResult.ok)
123
+ return { ok: false, error: targetingKeyResult.error };
124
+ const body = { targetingKey };
125
+ if (options.kind !== undefined)
126
+ body['kind'] = options.kind;
127
+ if (options.environment !== undefined)
128
+ body['environment'] = options.environment;
129
+ if (options.properties !== undefined && Object.keys(options.properties).length > 0) {
130
+ body['properties'] = options.properties;
131
+ }
132
+ // Retried once, and only when the taxonomy says a retry could succeed: a
133
+ // rejected payload or a bad key would be rejected identically.
134
+ let lastError;
135
+ for (let attempt = 0; attempt < 2; attempt += 1) {
136
+ try {
137
+ await this.requestJson(REGISTER_TARGET_PATH, body, options.signal);
138
+ return { ok: true };
139
+ }
140
+ catch (err) {
141
+ lastError = err instanceof errors_js_1.FireweaveError ? err : new errors_js_1.FireweaveError('BackendUnavailable');
142
+ if (!lastError.retryable)
143
+ break;
144
+ }
145
+ }
146
+ return { ok: false, ...(lastError !== undefined ? { error: lastError } : {}) };
147
+ }
148
+ recordExposure(exposure) {
149
+ if (this.closed || !this.ready)
150
+ return;
151
+ const event = {
152
+ type: 'exposure',
153
+ targetingKey: exposure.targetingKey,
154
+ controlPointKey: exposure.controlPointKey,
155
+ value: exposure.value,
156
+ };
157
+ if (exposure.variant !== undefined)
158
+ event.variant = exposure.variant;
159
+ if (exposure.rolloutId !== undefined)
160
+ event.rolloutId = exposure.rolloutId;
161
+ if (exposure.changeId !== undefined)
162
+ event.changeId = exposure.changeId;
163
+ if (exposure.stampId !== undefined)
164
+ event.stampId = exposure.stampId;
165
+ this.pending.push(event);
166
+ }
167
+ recordSignal(signal) {
168
+ if (this.closed || !this.ready)
169
+ return;
170
+ const event = {
171
+ type: 'signal',
172
+ targetingKey: signal.targetingKey ?? signal.rolloutId ?? 'fireweave-web-sdk',
173
+ name: signal.name,
174
+ properties: {
175
+ kind: signal.kind,
176
+ ...(signal.status !== undefined ? { status: signal.status } : {}),
177
+ ...(signal.errorKind !== undefined ? { errorKind: signal.errorKind } : {}),
178
+ ...(signal.message !== undefined ? { message: signal.message } : {}),
179
+ ...(signal.value !== undefined ? { value: signal.value } : {}),
180
+ },
181
+ };
182
+ if (signal.controlPointKey !== undefined)
183
+ event.controlPointKey = signal.controlPointKey;
184
+ if (signal.variant !== undefined)
185
+ event.variant = signal.variant;
186
+ if (signal.rolloutId !== undefined)
187
+ event.rolloutId = signal.rolloutId;
188
+ if (signal.timestamp !== undefined)
189
+ event.timestamp = signal.timestamp;
190
+ this.pending.push(event);
191
+ }
192
+ /**
193
+ * `beacon: true` is the unload path. It must not await anything the browser
194
+ * will cancel, so it uses `sendBeacon` when available and a `keepalive`
195
+ * fetch otherwise, and it never re-queues on failure — there is no later.
196
+ */
197
+ async flush(options = {}) {
198
+ if (this.closed || this.pending.length === 0)
199
+ return;
200
+ const batch = this.pending.splice(0, this.pending.length);
201
+ const payload = JSON.stringify({ events: batch });
202
+ if (options.beacon === true) {
203
+ this.sendBeaconOrKeepalive(payload);
204
+ return;
205
+ }
206
+ try {
207
+ await this.requestJson(CAPTURE_PATH, { events: batch });
208
+ }
209
+ catch {
210
+ // Never throw out of flush; re-queue so a later flush can retry.
211
+ this.pending.unshift(...batch);
212
+ }
213
+ }
214
+ sendBeaconOrKeepalive(payload) {
215
+ const url = `${this.apiUrl}${CAPTURE_PATH}`;
216
+ const nav = globalThis.navigator;
217
+ // sendBeacon cannot set an Authorization header, so it is only usable where
218
+ // the server accepts the key another way. Prefer keepalive fetch, which
219
+ // can, and fall back to a beacon only if fetch is unavailable.
220
+ try {
221
+ void this.fetchImpl(url, {
222
+ method: 'POST',
223
+ headers: { 'content-type': 'application/json', authorization: `Bearer ${this.apiKey}` },
224
+ body: payload,
225
+ keepalive: true,
226
+ }).catch(() => undefined);
227
+ return;
228
+ }
229
+ catch {
230
+ // fall through to beacon
231
+ }
232
+ if (typeof nav?.sendBeacon === 'function') {
233
+ try {
234
+ nav.sendBeacon(url, new Blob([payload], { type: 'application/json' }));
235
+ }
236
+ catch {
237
+ // Unload-time delivery is best-effort by definition.
238
+ }
239
+ }
240
+ }
241
+ async shutdown() {
242
+ if (this.closed)
243
+ return;
244
+ await this.flush();
245
+ this.closed = true;
246
+ this.ready = false;
247
+ }
248
+ features() {
249
+ return {
250
+ remoteEvaluation: true,
251
+ // Structural, not configurable — the browser never evaluates locally.
252
+ localEvaluation: false,
253
+ localOnly: false,
254
+ exposureEmission: true,
255
+ sideEffectFreeReads: true,
256
+ groupAnalytics: true,
257
+ };
258
+ }
259
+ toResolution(item, quotaLimited) {
260
+ if (item.found === false) {
261
+ return quotaLimited ? { found: false, quotaLimited: true } : { found: false };
262
+ }
263
+ const resolution = {
264
+ found: true,
265
+ value: item.value,
266
+ enabled: item.enabled ?? true,
267
+ };
268
+ if (item.variant != null)
269
+ resolution.variant = item.variant;
270
+ const reason = toReason(item.reason);
271
+ if (reason !== undefined)
272
+ resolution.reason = reason;
273
+ if (item.payload !== undefined)
274
+ resolution.payload = item.payload;
275
+ const meta = item.controlPointMetadata ?? {};
276
+ if (typeof meta['fireweave.controlPointVersion'] === 'number') {
277
+ resolution.version = meta['fireweave.controlPointVersion'];
278
+ }
279
+ if (typeof meta['fireweave.reasonCode'] === 'string') {
280
+ resolution.reasonCode = meta['fireweave.reasonCode'];
281
+ }
282
+ if (quotaLimited || meta['fireweave.quotaLimited'] === true)
283
+ resolution.quotaLimited = true;
284
+ return resolution;
285
+ }
286
+ async requestJson(path, body, outerSignal) {
287
+ const controller = new AbortController();
288
+ const timer = setTimeout(() => controller.abort(), this.requestTimeoutMs);
289
+ const onOuterAbort = () => controller.abort();
290
+ if (outerSignal !== undefined) {
291
+ if (outerSignal.aborted)
292
+ controller.abort();
293
+ else
294
+ outerSignal.addEventListener('abort', onOuterAbort, { once: true });
295
+ }
296
+ try {
297
+ let response;
298
+ try {
299
+ response = await this.fetchImpl(`${this.apiUrl}${path}`, {
300
+ method: 'POST',
301
+ headers: {
302
+ 'content-type': 'application/json',
303
+ authorization: `Bearer ${this.apiKey}`,
304
+ },
305
+ body: JSON.stringify(body),
306
+ signal: controller.signal,
307
+ });
308
+ }
309
+ catch (err) {
310
+ const aborted = err instanceof Error && err.name === 'AbortError';
311
+ throw aborted ? new errors_js_1.FireweaveError('Timeout') : new errors_js_1.FireweaveError('Network');
312
+ }
313
+ if (response.status >= 400)
314
+ throw mapHttpStatus(response.status);
315
+ try {
316
+ return (await response.json());
317
+ }
318
+ catch {
319
+ throw new errors_js_1.FireweaveError('MalformedResponse');
320
+ }
321
+ }
322
+ finally {
323
+ clearTimeout(timer);
324
+ if (outerSignal !== undefined)
325
+ outerSignal.removeEventListener('abort', onOuterAbort);
326
+ }
327
+ }
328
+ }
329
+ exports.FireweaveRemoteWebAdapter = FireweaveRemoteWebAdapter;
@@ -0,0 +1,10 @@
1
+ export declare const DEFAULT_ALLOWED_HOSTS: readonly string[];
2
+ export declare function isLoopbackHostname(hostname: string): boolean;
3
+ /**
4
+ * Validate a backend URL against the allowlist.
5
+ * Throws `FireweaveError('Configuration')` — fixed message, no host echoed,
6
+ * because this message can land in a shared browser console.
7
+ */
8
+ export declare function assertHostAllowed(host: string, allowedHosts?: readonly string[]): void;
9
+ /** Throw if `apiKey` looks like a vendor or secret key rather than a Fireweave project key. */
10
+ export declare function assertNotSecretKey(apiKey: string): void;
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_ALLOWED_HOSTS = void 0;
4
+ exports.isLoopbackHostname = isLoopbackHostname;
5
+ exports.assertHostAllowed = assertHostAllowed;
6
+ exports.assertNotSecretKey = assertNotSecretKey;
7
+ /**
8
+ * Backend host allowlist — the same SSRF guard the server SDK applies, kept
9
+ * for a different reason.
10
+ *
11
+ * A browser cannot be tricked into reaching a cloud metadata endpoint the way a
12
+ * server can; the browser's own origin model already prevents that. What this
13
+ * guard buys here is narrower and still worth having: a mistyped or tampered
14
+ * `apiUrl` cannot quietly send a project key, a targeting key, and a user's
15
+ * evaluation attributes to an unintended host. The allowlist is ON by default,
16
+ * so reaching a self-hosted fw-server is a deliberate, explicit act.
17
+ *
18
+ * Scheme policy matches the server SDK: https everywhere except loopback,
19
+ * where plain http is allowed for local development against a dev server.
20
+ */
21
+ const errors_js_1 = require("../domain/errors.js");
22
+ exports.DEFAULT_ALLOWED_HOSTS = Object.freeze([
23
+ 'app-server.fireweave.ai',
24
+ 'staging-app-server.fireweave.ai',
25
+ 'localhost',
26
+ '127.0.0.1',
27
+ '::1',
28
+ ]);
29
+ function isLoopbackHostname(hostname) {
30
+ return (hostname === 'localhost' ||
31
+ hostname === '127.0.0.1' ||
32
+ hostname === '::1' ||
33
+ hostname === '[::1]');
34
+ }
35
+ /**
36
+ * Validate a backend URL against the allowlist.
37
+ * Throws `FireweaveError('Configuration')` — fixed message, no host echoed,
38
+ * because this message can land in a shared browser console.
39
+ */
40
+ function assertHostAllowed(host, allowedHosts) {
41
+ let url;
42
+ try {
43
+ url = new URL(host);
44
+ }
45
+ catch {
46
+ throw new errors_js_1.FireweaveError('Configuration');
47
+ }
48
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') {
49
+ throw new errors_js_1.FireweaveError('Configuration');
50
+ }
51
+ const loopback = isLoopbackHostname(url.hostname);
52
+ if (url.protocol === 'http:' && !loopback) {
53
+ throw new errors_js_1.FireweaveError('Configuration');
54
+ }
55
+ const list = allowedHosts !== undefined && allowedHosts.length > 0 ? allowedHosts : exports.DEFAULT_ALLOWED_HOSTS;
56
+ if (list.includes('*'))
57
+ return; // explicit opt-out
58
+ const allowed = list.some((h) => url.hostname === h ||
59
+ (h === 'localhost' && loopback) ||
60
+ (h === '::1' && url.hostname === '[::1]'));
61
+ if (!allowed) {
62
+ throw new errors_js_1.FireweaveError('Configuration');
63
+ }
64
+ }
65
+ /**
66
+ * Key shapes this SDK refuses to accept.
67
+ *
68
+ * The whole security argument for a browser package (ADR-0009) is that it never
69
+ * wants a secret. Enforcing that at the door — rather than trusting that no
70
+ * code path asks for one — turns the claim into something a caller cannot
71
+ * accidentally violate by pasting the wrong value into their bundle config.
72
+ */
73
+ const FORBIDDEN_KEY_PREFIXES = Object.freeze(['phs_', 'phx_', 'phc_']);
74
+ /** Throw if `apiKey` looks like a vendor or secret key rather than a Fireweave project key. */
75
+ function assertNotSecretKey(apiKey) {
76
+ const key = apiKey.trim();
77
+ if (key.length === 0)
78
+ throw new errors_js_1.FireweaveError('Configuration');
79
+ for (const prefix of FORBIDDEN_KEY_PREFIXES) {
80
+ if (key.startsWith(prefix)) {
81
+ throw new errors_js_1.FireweaveError('Configuration');
82
+ }
83
+ }
84
+ }
@@ -0,0 +1,2 @@
1
+ export declare const SDK_VERSION = "3.0.0";
2
+ export declare const SDK_CHANNEL: 'staging' | 'production';
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SDK_CHANNEL = exports.SDK_VERSION = void 0;
4
+ // GENERATED by tools/release/version.sh apply web — do not edit by hand.
5
+ // The start profile reads SDK_CHANNEL to choose its default fw-server host
6
+ // (docs/adr/0012-start-profile.md). test/unit/start-build-info.test.ts pins
7
+ // SDK_VERSION to package.json, so a release that forgets to stamp fails CI.
8
+ exports.SDK_VERSION = '3.0.0';
9
+ exports.SDK_CHANNEL = 'production';
@@ -0,0 +1,24 @@
1
+ export interface ControlPointDefinition {
2
+ /** Value served in local mode. Ignored in remote mode. */
3
+ readonly local: boolean;
4
+ /** Optional note for humans and agents. Never sent anywhere. */
5
+ readonly description?: string;
6
+ }
7
+ export type ControlPointMap = Readonly<Record<string, ControlPointDefinition>>;
8
+ /**
9
+ * Validate a control-points object and return a frozen copy. Throws a TypeError naming
10
+ * the bad entry: the core's FireweaveError carries fixed messages only, and a
11
+ * typo here should say which key it was.
12
+ */
13
+ export declare function normalizeControlPoints(controlPoints: unknown): ControlPointMap;
14
+ /**
15
+ * Declare the app's control points. Returns its argument unchanged, typed, and
16
+ * checks it at import time so a typo fails where it was made.
17
+ *
18
+ * export const controlPoints = defineControlPoints({
19
+ * 'new-checkout': { local: true },
20
+ * });
21
+ */
22
+ export declare function defineControlPoints<const T extends Record<string, ControlPointDefinition>>(controlPoints: T): T;
23
+ /** The core local adapter's seed map. */
24
+ export declare function toLocalControlPoints(controlPoints: ControlPointMap): Record<string, boolean>;
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.normalizeControlPoints = normalizeControlPoints;
4
+ exports.defineControlPoints = defineControlPoints;
5
+ exports.toLocalControlPoints = toLocalControlPoints;
6
+ /**
7
+ * The control-points object: every control point the app reads, with the value
8
+ * served in local mode. It lives in its own file
9
+ * (src/fireweave/control-points.ts by default) and is passed as
10
+ * start({ controlPoints }) — the same shape as the server SDK.
11
+ *
12
+ * It holds local values only. In remote mode fw-server and the rollout decide,
13
+ * and call sites keep `false` as their default (RAMP-1), so this file can
14
+ * never switch a feature on for real visitors.
15
+ */
16
+ const index_js_1 = require("../index.js");
17
+ /**
18
+ * Validate a control-points object and return a frozen copy. Throws a TypeError naming
19
+ * the bad entry: the core's FireweaveError carries fixed messages only, and a
20
+ * typo here should say which key it was.
21
+ */
22
+ function normalizeControlPoints(controlPoints) {
23
+ if (controlPoints === undefined || controlPoints === null)
24
+ return Object.freeze({});
25
+ if (typeof controlPoints !== 'object' || Array.isArray(controlPoints)) {
26
+ throw new TypeError('[fireweave] start({ controlPoints }) must be an object of { key: { local: boolean } }.');
27
+ }
28
+ const out = {};
29
+ for (const [key, entry] of Object.entries(controlPoints)) {
30
+ if (!(0, index_js_1.validateControlPointKey)(key).ok)
31
+ throw new TypeError(`[fireweave] controlPoints: '${key}' is not a valid control point key.`);
32
+ if (typeof entry !== 'object' || entry === null || typeof entry.local !== 'boolean') {
33
+ throw new TypeError(`[fireweave] controlPoints['${key}'] must be { local: true } or { local: false }.`);
34
+ }
35
+ const { local, description } = entry;
36
+ if (description !== undefined && typeof description !== 'string') {
37
+ throw new TypeError(`[fireweave] controlPoints['${key}'].description must be a string.`);
38
+ }
39
+ out[key] = Object.freeze(description === undefined ? { local } : { local, description });
40
+ }
41
+ return Object.freeze(out);
42
+ }
43
+ /**
44
+ * Declare the app's control points. Returns its argument unchanged, typed, and
45
+ * checks it at import time so a typo fails where it was made.
46
+ *
47
+ * export const controlPoints = defineControlPoints({
48
+ * 'new-checkout': { local: true },
49
+ * });
50
+ */
51
+ function defineControlPoints(controlPoints) {
52
+ normalizeControlPoints(controlPoints);
53
+ return controlPoints;
54
+ }
55
+ /** The core local adapter's seed map. */
56
+ function toLocalControlPoints(controlPoints) {
57
+ const out = {};
58
+ for (const [key, entry] of Object.entries(controlPoints))
59
+ out[key] = entry.local;
60
+ return out;
61
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * `fw`: the page-wide accessor beside start(). Safe to import anywhere, in any
3
+ * order, and to destructure at module scope.
4
+ *
5
+ * Reads are synchronous (a browser renders from a prefetched cache) and never
6
+ * throw. Before start settles, or if it failed, they return the caller's
7
+ * default, and the *Details forms an ERROR decision saying why.
8
+ */
9
+ import { type FireweaveWebClient, type JsonValue, type RegisterTargetResult, type TargetKind, type WebControlPointsApi } from '../index.js';
10
+ import { type Persistence } from './identity.js';
11
+ import { type FireweaveWebStatus, type StartState } from './state.js';
12
+ /** The same nine read methods as the core client's controlPoints. */
13
+ export type ControlPoints = Pick<WebControlPointsApi, 'evaluate' | 'getBooleanValue' | 'getStringValue' | 'getNumberValue' | 'getObjectValue' | 'getBooleanDetails' | 'getStringDetails' | 'getNumberDetails' | 'getObjectDetails'>;
14
+ export interface IdentifyOptions {
15
+ /** Defaults to 'user'. */
16
+ readonly kind?: TargetKind;
17
+ }
18
+ export interface FireweaveWebStart {
19
+ /** Read control points: `fw.controlPoints.getBooleanValue('key', false)`. Synchronous. */
20
+ readonly controlPoints: ControlPoints;
21
+ /** Sign-in and session restore: register the user, then switch decisions to their key. Never throws. */
22
+ identify(targetingKey: string, properties?: Record<string, JsonValue>, options?: IdentifyOptions): Promise<RegisterTargetResult>;
23
+ /** Sign-out: back to this browser's device id. Never throws. */
24
+ reset(): Promise<void>;
25
+ /** The anonymous key, for joining with analytics. Undefined before start. */
26
+ deviceId(): string | undefined;
27
+ /** Change storage at runtime, for consent: 'memory' deletes what was stored. */
28
+ setPersistence(mode: Persistence): void;
29
+ /** Withdraw consent and start over: clears storage and mints a fresh in-memory device id. */
30
+ forget(): Promise<void>;
31
+ /** Settles when the current start attempt does. Never rejects; already resolved when nothing is starting. */
32
+ readonly ready: Promise<void>;
33
+ /** What start() decided and how it went: mode, channel, host, key source, problem. Never the key. */
34
+ status(): FireweaveWebStatus;
35
+ /** Called on every state change, including the re-prefetch after identify or reset. Returns unsubscribe. */
36
+ subscribe(listener: (state: StartState) => void): () => void;
37
+ /** The core client, for anything the facade does not cover. Undefined until start settles. */
38
+ client(): FireweaveWebClient | undefined;
39
+ /** Flush and close. A later start() begins fresh. */
40
+ shutdown(): Promise<void>;
41
+ }
42
+ export declare const fw: FireweaveWebStart;