@coframe/sdk 1.1.46 → 1.1.48

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.
@@ -1,3 +1,83 @@
1
+ const LAST_VISIT_TIME_KEY = 'coframe.lastVisitTime';
2
+ const VISIT_COUNT_COOKIE_KEY = 'coframe.cvc';
3
+ const LANE_COOKIE_KEY = 'coframe.lanes';
4
+ function readCookieHeader(cookieHeader, name) {
5
+ if (!cookieHeader)
6
+ return undefined;
7
+ let value;
8
+ for (const part of cookieHeader.split(';')) {
9
+ const entry = part.trim();
10
+ const separator = entry.indexOf('=');
11
+ if (separator >= 0 && entry.slice(0, separator) === name) {
12
+ value = entry.slice(separator + 1);
13
+ }
14
+ }
15
+ return value;
16
+ }
17
+ function getGlobalScope() {
18
+ if (typeof globalThis !== 'undefined') {
19
+ return globalThis;
20
+ }
21
+ if (typeof window !== 'undefined') {
22
+ return window;
23
+ }
24
+ if (typeof self !== 'undefined') {
25
+ return self;
26
+ }
27
+ }
28
+ const generateUUID = () => {
29
+ var _a;
30
+ const cryptoObject = (_a = getGlobalScope()) === null || _a === void 0 ? void 0 : _a.crypto;
31
+ if (cryptoObject === null || cryptoObject === void 0 ? void 0 : cryptoObject.randomUUID)
32
+ return cryptoObject.randomUUID();
33
+ const randomByte = (cryptoObject === null || cryptoObject === void 0 ? void 0 : cryptoObject.getRandomValues)
34
+ ? () => cryptoObject.getRandomValues(new Uint8Array(1))[0]
35
+ : () => Math.floor(Math.random() * 256);
36
+ return `${1e7}${ -1e3}${ -4e3}${ -8e3}${ -1e11}`.replace(/[018]/g, (c) => {
37
+ const randomValue = randomByte();
38
+ return (c ^
39
+ (randomValue & (15 >> (c / 4)))).toString(16);
40
+ });
41
+ };
42
+ const VISIT_BOUNDARY_MS = 2 * 60 * 60 * 1000;
43
+ function startsNewVisit(lastVisitTime, now) {
44
+ return !lastVisitTime || now - lastVisitTime > VISIT_BOUNDARY_MS;
45
+ }
46
+ function nextVisitCount(existing, lastVisitTime, now) {
47
+ return startsNewVisit(lastVisitTime, now) ? existing + 1 : existing;
48
+ }
49
+ function parseNonNegativeInt(raw) {
50
+ if (!raw || !/^\d+$/.test(raw))
51
+ return undefined;
52
+ const parsed = Number(raw);
53
+ return parsed <= 9007199254740991 ? parsed : undefined;
54
+ }
55
+ function parseVisitCount(raw) {
56
+ var _a;
57
+ return (_a = parseNonNegativeInt(raw)) !== null && _a !== void 0 ? _a : 0;
58
+ }
59
+
60
+ function isRecord(value) {
61
+ return !!value && typeof value === 'object' && !Array.isArray(value);
62
+ }
63
+
64
+ function inferBrowserPlatform(userAgent) {
65
+ const ua = userAgent !== null && userAgent !== void 0 ? userAgent : '';
66
+ if (/iphone/i.test(ua))
67
+ return 'iPhone';
68
+ if (/ipad/i.test(ua))
69
+ return 'iPad';
70
+ if (/android/i.test(ua))
71
+ return 'Linux armv8l';
72
+ if (/windows/i.test(ua))
73
+ return 'Win32';
74
+ if (/macintosh|mac os/i.test(ua))
75
+ return 'MacIntel';
76
+ if (/cros|linux/i.test(ua))
77
+ return 'Linux x86_64';
78
+ return '';
79
+ }
80
+
1
81
  const MAX_USER_CONTEXT_JSON_BYTES = 4096;
2
82
  function userContextJsonByteLength(userContext) {
3
83
  try {
@@ -20,29 +100,152 @@ function sanitizeUserContext(userContext) {
20
100
  return userContext;
21
101
  }
22
102
 
23
- function getGlobalScope() {
24
- if (typeof globalThis !== 'undefined') {
25
- return globalThis;
26
- }
27
- if (typeof window !== 'undefined') {
28
- return window;
103
+ const ASSIGNMENT_FIELD_LIMITS = {
104
+ projectId: 200,
105
+ experimentId: 512,
106
+ userToken: 512,
107
+ currentUrl: 4096,
108
+ referrer: 4096,
109
+ userAgent: 2048,
110
+ browserPlatform: 128,
111
+ country: 8,
112
+ region: 128,
113
+ city: 256,
114
+ verifiedBotCategory: 128,
115
+ clientIp: 64,
116
+ languages: 20,
117
+ languageLength: 64,
118
+ botScoreMin: 1,
119
+ botScoreMax: 99,
120
+ };
121
+ const ASSIGNMENT_DEVICES = [
122
+ 'desktop',
123
+ 'mobile',
124
+ 'tablet',
125
+ ];
126
+ function isInvalidString(value, max) {
127
+ return (value !== undefined && (typeof value !== 'string' || value.length > max));
128
+ }
129
+ function isInvalidLanguages(value) {
130
+ return (value !== undefined &&
131
+ (!Array.isArray(value) ||
132
+ value.length > ASSIGNMENT_FIELD_LIMITS.languages ||
133
+ !value.every((language) => typeof language === 'string' &&
134
+ language.length <= ASSIGNMENT_FIELD_LIMITS.languageLength)));
135
+ }
136
+ function invalidUserContext(value) {
137
+ if (value === undefined)
138
+ return null;
139
+ if (!isRecord(value)) {
140
+ return { field: 'userContext', message: 'must be an object' };
29
141
  }
30
- if (typeof self !== 'undefined') {
31
- return self;
142
+ if (userContextJsonByteLength(value) > MAX_USER_CONTEXT_JSON_BYTES) {
143
+ return { field: 'userContext', message: 'is too large', tooLarge: true };
32
144
  }
145
+ return null;
33
146
  }
34
- const generateUUID = () => {
35
- var _a;
36
- const cryptoObject = (_a = getGlobalScope()) === null || _a === void 0 ? void 0 : _a.crypto;
37
- if (cryptoObject === null || cryptoObject === void 0 ? void 0 : cryptoObject.randomUUID)
38
- return cryptoObject.randomUUID();
39
- return `${1e7}${ -1e3}${ -4e3}${ -8e3}${ -1e11}`.replace(/[018]/g, (c) => {
40
- var _a, _b;
41
- const randomValue = (_b = (_a = cryptoObject === null || cryptoObject === void 0 ? void 0 : cryptoObject.getRandomValues) === null || _a === void 0 ? void 0 : _a.call(cryptoObject, new Uint8Array(1))[0]) !== null && _b !== void 0 ? _b : 0;
42
- return (c ^
43
- (randomValue & (15 >> (c / 4)))).toString(16);
44
- });
147
+ const ASSIGNMENT_CONTEXT_HEADERS = {
148
+ userAgent: 'user-agent',
149
+ country: 'cf-ipcountry',
150
+ region: 'cf-region-code',
151
+ city: 'cf-ipcity',
152
+ device: 'cf-device-type',
153
+ botScore: 'cf-bot-score',
154
+ verifiedBotCategory: 'cf-verified-bot-category',
155
+ clientIp: 'cf-connecting-ip',
45
156
  };
157
+ const HEADER_STRING_FIELDS = [
158
+ 'userAgent',
159
+ 'country',
160
+ 'region',
161
+ 'city',
162
+ 'verifiedBotCategory',
163
+ 'clientIp',
164
+ ];
165
+ const CONTEXT_STRING_FIELDS = [
166
+ 'referrer',
167
+ 'userAgent',
168
+ 'browserPlatform',
169
+ 'country',
170
+ 'region',
171
+ 'city',
172
+ 'verifiedBotCategory',
173
+ 'clientIp',
174
+ ];
175
+ function invalidAssignmentContextField(context) {
176
+ if (!isRecord(context)) {
177
+ return { field: 'context', message: 'must be an object' };
178
+ }
179
+ const { currentUrl } = context;
180
+ if (typeof currentUrl !== 'string' || !currentUrl) {
181
+ return { field: 'currentUrl', message: 'is required' };
182
+ }
183
+ if (currentUrl.length > ASSIGNMENT_FIELD_LIMITS.currentUrl) {
184
+ return {
185
+ field: 'currentUrl',
186
+ message: `must be at most ${ASSIGNMENT_FIELD_LIMITS.currentUrl} characters`,
187
+ };
188
+ }
189
+ try {
190
+ new URL(currentUrl);
191
+ }
192
+ catch (_a) {
193
+ return { field: 'currentUrl', message: 'must be an absolute URL' };
194
+ }
195
+ for (const field of CONTEXT_STRING_FIELDS) {
196
+ const max = ASSIGNMENT_FIELD_LIMITS[field];
197
+ if (isInvalidString(context[field], max)) {
198
+ return {
199
+ field,
200
+ message: `must be a string of at most ${max} characters`,
201
+ };
202
+ }
203
+ }
204
+ if (context.device !== undefined &&
205
+ !ASSIGNMENT_DEVICES.includes(context.device)) {
206
+ return { field: 'device', message: 'must be desktop, mobile, or tablet' };
207
+ }
208
+ const { botScore, languages, userVisitCount, userContext } = context;
209
+ if (botScore !== undefined &&
210
+ (!Number.isInteger(botScore) ||
211
+ botScore < ASSIGNMENT_FIELD_LIMITS.botScoreMin ||
212
+ botScore > ASSIGNMENT_FIELD_LIMITS.botScoreMax)) {
213
+ return {
214
+ field: 'botScore',
215
+ message: `must be an integer from ${ASSIGNMENT_FIELD_LIMITS.botScoreMin} to ${ASSIGNMENT_FIELD_LIMITS.botScoreMax}`,
216
+ };
217
+ }
218
+ if (isInvalidLanguages(languages)) {
219
+ return {
220
+ field: 'languages',
221
+ message: `must contain at most ${ASSIGNMENT_FIELD_LIMITS.languages} strings of ${ASSIGNMENT_FIELD_LIMITS.languageLength} characters`,
222
+ };
223
+ }
224
+ if (userVisitCount !== undefined &&
225
+ (!Number.isSafeInteger(userVisitCount) || userVisitCount < 0)) {
226
+ return {
227
+ field: 'userVisitCount',
228
+ message: 'must be a non-negative safe integer',
229
+ };
230
+ }
231
+ return invalidUserContext(userContext);
232
+ }
233
+ const ID_FIELDS = ['projectId', 'experimentId', 'userToken'];
234
+ function invalidAssignmentIdField(body) {
235
+ for (const field of ID_FIELDS) {
236
+ const value = body[field];
237
+ if (typeof value !== 'string' || !value) {
238
+ return { field, message: 'is required' };
239
+ }
240
+ if (value.length > ASSIGNMENT_FIELD_LIMITS[field]) {
241
+ return {
242
+ field,
243
+ message: `must be at most ${ASSIGNMENT_FIELD_LIMITS[field]} characters`,
244
+ };
245
+ }
246
+ }
247
+ return null;
248
+ }
46
249
 
47
250
  async function fetchProjectFromEdge(projectId, currentUrl, signal, userContext, previewVariantId, headers, referrer) {
48
251
  const payload = { projectId, currentUrl };
@@ -310,12 +513,189 @@ async function flushFeatureFlagEvents() {
310
513
  }
311
514
  }
312
515
 
516
+ const DEFAULT_ASSIGNMENT_TIMEOUT_MS = 1500;
517
+ function normalizeRequestHeaders(headers) {
518
+ const normalized = new Headers();
519
+ if (typeof (headers === null || headers === void 0 ? void 0 : headers.forEach) === 'function') {
520
+ headers.forEach((value, name) => {
521
+ normalized.set(name, value);
522
+ });
523
+ return normalized;
524
+ }
525
+ for (const [name, value] of Object.entries(headers !== null && headers !== void 0 ? headers : {})) {
526
+ const joined = typeof value === 'string'
527
+ ? value
528
+ : Array.isArray(value)
529
+ ? value.join(name.toLowerCase() === 'cookie' ? '; ' : ', ')
530
+ : undefined;
531
+ if (joined === undefined)
532
+ continue;
533
+ try {
534
+ normalized.set(name, joined);
535
+ }
536
+ catch (_a) { }
537
+ }
538
+ return normalized;
539
+ }
540
+ function createAssignmentHeaders(incoming) {
541
+ const headers = new Headers({ 'Content-Type': 'application/json' });
542
+ const laneCookie = readCookieHeader(incoming.get('cookie'), LANE_COOKIE_KEY);
543
+ if (laneCookie !== undefined) {
544
+ headers.set('cookie', `${LANE_COOKIE_KEY}=${laneCookie}`);
545
+ }
546
+ return headers;
547
+ }
548
+ function requestLanguages(headers) {
549
+ var _a;
550
+ const languages = ((_a = headers.get('accept-language')) !== null && _a !== void 0 ? _a : '')
551
+ .split(',')
552
+ .map((entry) => { var _a; return (_a = entry.split(';')[0]) === null || _a === void 0 ? void 0 : _a.trim(); })
553
+ .filter((entry) => !!entry && entry.length <= ASSIGNMENT_FIELD_LIMITS.languageLength)
554
+ .slice(0, ASSIGNMENT_FIELD_LIMITS.languages);
555
+ return languages.length > 0 ? languages : undefined;
556
+ }
557
+ function clampHeader(value, maxLength) {
558
+ if (!value)
559
+ return undefined;
560
+ return value.length > maxLength ? value.slice(0, maxLength) : value;
561
+ }
562
+ function headerStrings(options, incoming) {
563
+ var _a;
564
+ const values = {};
565
+ for (const field of HEADER_STRING_FIELDS) {
566
+ const value = (_a = options[field]) !== null && _a !== void 0 ? _a : clampHeader(incoming.get(ASSIGNMENT_CONTEXT_HEADERS[field]), ASSIGNMENT_FIELD_LIMITS[field]);
567
+ if (value)
568
+ values[field] = value;
569
+ }
570
+ return values;
571
+ }
572
+ function requestDevice(headers) {
573
+ var _a;
574
+ const device = (_a = headers.get(ASSIGNMENT_CONTEXT_HEADERS.device)) === null || _a === void 0 ? void 0 : _a.toLowerCase();
575
+ return ASSIGNMENT_DEVICES.includes(device)
576
+ ? device
577
+ : undefined;
578
+ }
579
+ function requestBotScore(headers) {
580
+ const score = parseNonNegativeInt(headers.get(ASSIGNMENT_CONTEXT_HEADERS.botScore));
581
+ return score !== undefined &&
582
+ score >= ASSIGNMENT_FIELD_LIMITS.botScoreMin &&
583
+ score <= ASSIGNMENT_FIELD_LIMITS.botScoreMax
584
+ ? score
585
+ : undefined;
586
+ }
587
+ function edgeBaseUrl(edgeUrl) {
588
+ return (edgeUrl || "https://edge.cofra.me").replace(/\/+$/, '');
589
+ }
590
+ function validateAssignmentContextOptions(options) {
591
+ const problem = invalidAssignmentContextField(options);
592
+ if (problem)
593
+ throw new Error(`${problem.field} ${problem.message}`);
594
+ }
595
+ function effectiveVisitCount(headers) {
596
+ const cookie = headers.get('cookie');
597
+ const existing = parseVisitCount(readCookieHeader(cookie, VISIT_COUNT_COOKIE_KEY));
598
+ const lastVisitTime = parseVisitCount(readCookieHeader(cookie, LAST_VISIT_TIME_KEY));
599
+ const next = nextVisitCount(existing, lastVisitTime, Date.now());
600
+ return Number.isSafeInteger(next) ? next : existing;
601
+ }
602
+ function isExperimentAssignmentResult(value, experimentId) {
603
+ if (!isRecord(value) || !isRecord(value.projectData))
604
+ return false;
605
+ const { projectData } = value;
606
+ const context = projectData.assignmentContext;
607
+ if (!isRecord(projectData.project) ||
608
+ !Array.isArray(projectData.project.frames) ||
609
+ !isRecord(projectData.personalizationHeaders) ||
610
+ !isRecord(context) ||
611
+ !Array.isArray(context.languages) ||
612
+ !context.languages.every((language) => typeof language === 'string') ||
613
+ typeof context.browserPlatform !== 'string' ||
614
+ !Number.isSafeInteger(context.userVisitCount) ||
615
+ context.userVisitCount < 0) {
616
+ return false;
617
+ }
618
+ if (value.variant === null) {
619
+ return typeof value.reason === 'string';
620
+ }
621
+ if (!isRecord(value.variant))
622
+ return false;
623
+ return (typeof value.variant.id === 'string' &&
624
+ typeof value.variant.slug === 'string' &&
625
+ value.variant.experimentId === experimentId &&
626
+ typeof value.variant.iterationId === 'string');
627
+ }
628
+ function generateUserToken() {
629
+ return generateUUID();
630
+ }
631
+ async function getExperimentAssignment(options) {
632
+ var _a, _b, _c, _d, _e, _f;
633
+ const idProblem = invalidAssignmentIdField(options);
634
+ if (idProblem)
635
+ throw new Error(`${idProblem.field} ${idProblem.message}`);
636
+ if (options.timeoutMs !== undefined &&
637
+ !(Number.isFinite(options.timeoutMs) && options.timeoutMs > 0)) {
638
+ throw new Error('timeoutMs must be a positive finite number');
639
+ }
640
+ validateAssignmentContextOptions(options);
641
+ const currentUrl = new URL(options.currentUrl);
642
+ if (/cf_disable/.test(currentUrl.search))
643
+ return undefined;
644
+ const incoming = normalizeRequestHeaders(options.headers);
645
+ const controller = new AbortController();
646
+ const timeout = setTimeout(() => controller.abort(), (_a = options.timeoutMs) !== null && _a !== void 0 ? _a : DEFAULT_ASSIGNMENT_TIMEOUT_MS);
647
+ try {
648
+ const fromHeaders = headerStrings(options, incoming);
649
+ const languages = (_b = options.languages) !== null && _b !== void 0 ? _b : requestLanguages(incoming);
650
+ const browserPlatform = (_c = options.browserPlatform) !== null && _c !== void 0 ? _c : inferBrowserPlatform(fromHeaders.userAgent);
651
+ const device = (_d = options.device) !== null && _d !== void 0 ? _d : requestDevice(incoming);
652
+ const botScore = (_e = options.botScore) !== null && _e !== void 0 ? _e : requestBotScore(incoming);
653
+ const userVisitCount = (_f = options.userVisitCount) !== null && _f !== void 0 ? _f : effectiveVisitCount(incoming);
654
+ const referrer = options.referrer !== undefined
655
+ ? options.referrer
656
+ : clampHeader(incoming.get('referer'), ASSIGNMENT_FIELD_LIMITS.referrer);
657
+ const requestBody = {
658
+ projectId: options.projectId,
659
+ experimentId: options.experimentId,
660
+ userToken: options.userToken,
661
+ context: {
662
+ currentUrl: options.currentUrl,
663
+ ...(referrer !== undefined ? { referrer } : {}),
664
+ ...(options.userContext ? { userContext: options.userContext } : {}),
665
+ ...fromHeaders,
666
+ ...(languages !== undefined ? { languages } : {}),
667
+ ...(browserPlatform !== undefined ? { browserPlatform } : {}),
668
+ ...(device ? { device } : {}),
669
+ ...(botScore !== undefined ? { botScore } : {}),
670
+ userVisitCount,
671
+ },
672
+ };
673
+ const response = await fetch(`${edgeBaseUrl(options.edgeUrl)}/api/v1/assignment`, {
674
+ method: 'POST',
675
+ headers: createAssignmentHeaders(incoming),
676
+ signal: controller.signal,
677
+ body: JSON.stringify(requestBody),
678
+ });
679
+ if (!response.ok)
680
+ return undefined;
681
+ const result = await response.json();
682
+ return isExperimentAssignmentResult(result, options.experimentId)
683
+ ? result
684
+ : undefined;
685
+ }
686
+ catch (_g) {
687
+ return undefined;
688
+ }
689
+ finally {
690
+ clearTimeout(timeout);
691
+ }
692
+ }
313
693
  async function requestFeatureFlags(options) {
314
- var _a, _b;
694
+ var _a;
315
695
  const controller = new AbortController();
316
696
  const timeout = setTimeout(() => controller.abort(), (_a = options.requestTimeoutMs) !== null && _a !== void 0 ? _a : 3000);
317
697
  try {
318
- const response = await fetch(`${(_b = options.edgeUrl) !== null && _b !== void 0 ? _b : "https://edge.cofra.me"}/api/v1/flags?v=1`, {
698
+ const response = await fetch(`${edgeBaseUrl(options.edgeUrl)}/api/v1/flags?v=1`, {
319
699
  method: 'POST',
320
700
  headers: {
321
701
  'Content-Type': 'application/json',
@@ -343,7 +723,7 @@ async function requestFeatureFlags(options) {
343
723
  return undefined;
344
724
  return (await response.json());
345
725
  }
346
- catch (_c) {
726
+ catch (_b) {
347
727
  return undefined;
348
728
  }
349
729
  finally {
@@ -387,19 +767,6 @@ async function getFeatureFlagPayload(key, options) {
387
767
  return 'payload' in result ? ((_a = result.payload) !== null && _a !== void 0 ? _a : null) : null;
388
768
  }
389
769
  const getRemoteConfig = getFeatureFlagPayload;
390
- function getRequestReferrer(headers) {
391
- const get = headers.get;
392
- if (typeof get === 'function') {
393
- const value = get.call(headers, 'referer');
394
- return typeof value === 'string' ? value : undefined;
395
- }
396
- for (const [key, value] of Object.entries(headers)) {
397
- if (key.toLowerCase() === 'referer') {
398
- return typeof value === 'string' ? value : undefined;
399
- }
400
- }
401
- return undefined;
402
- }
403
770
  async function fetchAntiflickerFromEdge(projectId, currentUrl, referrer, userContext, antiflickerMode, headers) {
404
771
  try {
405
772
  const payload = { projectId, currentUrl };
@@ -427,23 +794,24 @@ async function fetchAntiflickerFromEdge(projectId, currentUrl, referrer, userCon
427
794
  }
428
795
  }
429
796
  async function getProjectData(options) {
430
- var _a;
797
+ var _a, _b;
431
798
  try {
432
799
  if (/cf_disable/.test(new URL(options.currentUrl).search)) {
433
800
  return null;
434
801
  }
435
802
  }
436
- catch (_b) {
803
+ catch (_c) {
437
804
  }
438
805
  const includeProject = options.includeProject !== false;
439
806
  const includeCSS = options.includeCSS !== false;
440
- const referrer = (_a = options.referrer) !== null && _a !== void 0 ? _a : getRequestReferrer(options.headers);
807
+ const headers = normalizeRequestHeaders(options.headers);
808
+ const referrer = (_b = (_a = options.referrer) !== null && _a !== void 0 ? _a : headers.get('referer')) !== null && _b !== void 0 ? _b : undefined;
441
809
  const [projectResult, antiflickerCSS] = await Promise.all([
442
810
  includeProject
443
- ? fetchProjectFromEdge(options.projectId, options.currentUrl, undefined, options.userContext, undefined, options.headers, referrer)
811
+ ? fetchProjectFromEdge(options.projectId, options.currentUrl, undefined, options.userContext, undefined, headers, referrer)
444
812
  : Promise.resolve(null),
445
813
  includeCSS
446
- ? fetchAntiflickerFromEdge(options.projectId, options.currentUrl, referrer, options.userContext, options.antiflickerMode, options.headers)
814
+ ? fetchAntiflickerFromEdge(options.projectId, options.currentUrl, referrer, options.userContext, options.antiflickerMode, headers)
447
815
  : Promise.resolve(null),
448
816
  ]);
449
817
  const config = {};
@@ -463,4 +831,4 @@ async function getProjectData(options) {
463
831
  return null;
464
832
  }
465
833
 
466
- export { evaluateFlags, flushFeatureFlagEvents, getFeatureFlag, getFeatureFlagPayload, getFeatureFlagResult, getProjectData, getRemoteConfig, isFeatureEnabled };
834
+ export { evaluateFlags, flushFeatureFlagEvents, generateUserToken, getExperimentAssignment, getFeatureFlag, getFeatureFlagPayload, getFeatureFlagResult, getProjectData, getRemoteConfig, isFeatureEnabled };
@@ -171,7 +171,7 @@ interface Project {
171
171
  }
172
172
  interface Frame {
173
173
  activation_condition: ActivationCondition;
174
- experiments: Experiment[];
174
+ experiments?: Experiment[];
175
175
  elements: CoframeElements;
176
176
  }
177
177
  interface Experiment {
@@ -298,9 +298,16 @@ type PersonalizationHeaders = {
298
298
  'x-bot-reason': BotReason;
299
299
  };
300
300
 
301
+ interface AssignmentEvaluationContext {
302
+ languages: string[];
303
+ browserPlatform: string;
304
+ userVisitCount: number;
305
+ }
306
+
301
307
  interface ProjectData {
302
308
  project: Project;
303
309
  personalizationHeaders?: Partial<PersonalizationHeaders>;
310
+ assignmentContext?: AssignmentEvaluationContext;
304
311
  }
305
312
  interface CoframeInitOptions {
306
313
  config?: CoframeConfig;
@@ -191,7 +191,7 @@ interface Project {
191
191
  }
192
192
  interface Frame {
193
193
  activation_condition: ActivationCondition;
194
- experiments: Experiment[];
194
+ experiments?: Experiment[];
195
195
  elements: CoframeElements;
196
196
  }
197
197
  interface Experiment {
@@ -312,9 +312,16 @@ type PersonalizationHeaders = {
312
312
  'x-bot-reason': BotReason;
313
313
  };
314
314
 
315
+ interface AssignmentEvaluationContext {
316
+ languages: string[];
317
+ browserPlatform: string;
318
+ userVisitCount: number;
319
+ }
320
+
315
321
  interface ProjectData {
316
322
  project: Project;
317
323
  personalizationHeaders?: Partial<PersonalizationHeaders>;
324
+ assignmentContext?: AssignmentEvaluationContext;
318
325
  }
319
326
  type LogPayload = {
320
327
  kind: 'warn' | 'error' | 'exception';
@@ -137,7 +137,7 @@ interface Project {
137
137
  }
138
138
  interface Frame {
139
139
  activation_condition: ActivationCondition;
140
- experiments: Experiment[];
140
+ experiments?: Experiment[];
141
141
  elements: CoframeElements;
142
142
  }
143
143
  interface Experiment {
@@ -234,6 +234,50 @@ type PersonalizationHeaders = {
234
234
  'x-bot-reason': BotReason;
235
235
  };
236
236
 
237
+ type AssignmentDevice = 'desktop' | 'mobile' | 'tablet';
238
+ interface AssignmentContext {
239
+ currentUrl: string;
240
+ referrer?: string;
241
+ userContext?: Record<string, unknown>;
242
+ userAgent?: string;
243
+ languages?: string[];
244
+ browserPlatform?: string;
245
+ country?: string;
246
+ region?: string;
247
+ city?: string;
248
+ device?: AssignmentDevice;
249
+ botScore?: number;
250
+ verifiedBotCategory?: string;
251
+ clientIp?: string;
252
+ userVisitCount?: number;
253
+ }
254
+ interface AssignmentEvaluationContext {
255
+ languages: string[];
256
+ browserPlatform: string;
257
+ userVisitCount: number;
258
+ }
259
+ interface AssignmentProjectData {
260
+ project: Project;
261
+ personalizationHeaders: PersonalizationHeaders;
262
+ assignmentContext: AssignmentEvaluationContext;
263
+ }
264
+ interface AssignedVariant$1 {
265
+ id: string;
266
+ slug: string;
267
+ experimentId: string;
268
+ iterationId: string;
269
+ }
270
+ type AssignmentMissReason$1 = 'experiment_not_found' | 'not_assigned' | 'unsupported_experiment_type' | 'preview_active';
271
+ type AssignmentOutcome<Reason extends string = AssignmentMissReason$1> = {
272
+ variant: AssignedVariant$1;
273
+ } | {
274
+ variant: null;
275
+ reason: Reason;
276
+ };
277
+ type AssignmentResponse<Reason extends string = AssignmentMissReason$1> = AssignmentOutcome<Reason> & {
278
+ projectData: AssignmentProjectData;
279
+ };
280
+
237
281
  type JsonValue = string | number | boolean | null | JsonValue[] | {
238
282
  [key: string]: JsonValue;
239
283
  };
@@ -262,12 +306,24 @@ interface FeatureFlagSelector {
262
306
 
263
307
  declare function flushFeatureFlagEvents(): Promise<void>;
264
308
 
309
+ type RequestHeaders = Headers | Record<string, string | string[] | undefined>;
310
+ interface GetExperimentAssignmentOptions extends AssignmentContext {
311
+ projectId: string;
312
+ experimentId: string;
313
+ userToken: string;
314
+ headers: RequestHeaders;
315
+ timeoutMs?: number;
316
+ edgeUrl?: string;
317
+ }
318
+ type AssignedVariant = AssignedVariant$1;
319
+ type AssignmentMissReason = AssignmentMissReason$1 | (string & {});
320
+ type ExperimentAssignmentResult = AssignmentResponse<AssignmentMissReason>;
265
321
  interface GetProjectDataOptions {
266
322
  projectId: string;
267
323
  currentUrl: string;
268
324
  referrer?: string;
269
325
  userContext?: Record<string, unknown>;
270
- headers: Headers | Record<string, string>;
326
+ headers: RequestHeaders;
271
327
  antiflickerMode?: AntiflickerMode;
272
328
  includeProject?: boolean;
273
329
  includeCSS?: boolean;
@@ -277,6 +333,8 @@ interface CoframeProjectPayload {
277
333
  personalizationHeaders?: PersonalizationHeaders;
278
334
  antiflickerCSS?: string;
279
335
  }
336
+ declare function generateUserToken(): string;
337
+ declare function getExperimentAssignment(options: GetExperimentAssignmentOptions): Promise<ExperimentAssignmentResult | undefined>;
280
338
  interface EvaluateFlagsOptions {
281
339
  apiKey: string;
282
340
  projectId: string;
@@ -306,5 +364,5 @@ declare const getRemoteConfig: typeof getFeatureFlagPayload;
306
364
 
307
365
  declare function getProjectData(options: GetProjectDataOptions): Promise<CoframeProjectPayload | null>;
308
366
 
309
- export { evaluateFlags, flushFeatureFlagEvents, getFeatureFlag, getFeatureFlagPayload, getFeatureFlagResult, getProjectData, getRemoteConfig, isFeatureEnabled };
310
- export type { CoframeProjectPayload, EvaluateFlagsOptions, FeatureFlagServerReadOptions, GetProjectDataOptions };
367
+ export { evaluateFlags, flushFeatureFlagEvents, generateUserToken, getExperimentAssignment, getFeatureFlag, getFeatureFlagPayload, getFeatureFlagResult, getProjectData, getRemoteConfig, isFeatureEnabled };
368
+ export type { AssignedVariant, AssignmentMissReason, CoframeProjectPayload, EvaluateFlagsOptions, ExperimentAssignmentResult, FeatureFlagServerReadOptions, GetExperimentAssignmentOptions, GetProjectDataOptions, RequestHeaders };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coframe/sdk",
3
- "version": "1.1.46",
3
+ "version": "1.1.48",
4
4
  "description": "Coframe optimizes your website using generative AI to deliver maximum impact to your audience and allow your site to improve itself as you sleep. Learn more at coframe.com.",
5
5
  "main": "dist/pkg/cf.cjs.js",
6
6
  "module": "dist/pkg/cf.esm.mjs",