@coframe/sdk 1.1.47 → 1.1.49

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