@el7ven/cookie-kit 0.3.1 → 0.3.3
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.
- package/package.json +21 -1
- package/src/core/AnalyticsManager.js +400 -0
- package/src/core/ConsentManager.js +710 -0
- package/src/core/ConsentMode.js +109 -0
- package/src/core/FocusTrap.js +130 -0
- package/src/core/GeoDetector.js +144 -0
- package/src/core/ScriptLoader.js +229 -0
- package/src/core/StorageAdapter.js +179 -0
- package/src/core/index.js +4 -4
- package/src/geo/GeoDetector.js +536 -0
- package/src/geo/RegionRules.js +126 -0
- package/src/geo/index.js +16 -0
- package/src/index.js +55 -17
- package/src/locales/en.js +54 -0
- package/src/locales/index.js +20 -0
- package/src/locales/ro.js +54 -0
- package/src/plugins/CMPPlugin.js +187 -0
- package/src/plugins/PluginManager.js +234 -0
- package/src/plugins/index.js +7 -0
- package/src/providers/GoogleConsentModeProvider.js +278 -0
- package/src/providers/index.js +6 -0
- package/src/rewriting/ScriptRewriter.js +278 -0
- package/src/rewriting/index.js +6 -0
- package/src/scripts/ScriptLoader.js +310 -0
- package/src/scripts/ScriptManager.js +278 -0
- package/src/scripts/ScriptRegistry.js +175 -0
- package/src/scripts/ScriptScanner.js +178 -0
- package/src/scripts/index.js +9 -0
- package/src/trackers/TrackerDetector.js +488 -0
- package/src/trackers/TrackerPatterns.js +307 -0
- package/src/trackers/TrackerRegistry.js +172 -0
- package/src/trackers/index.js +15 -0
- package/src/utils/cookies.js +37 -0
- package/src/utils/dom.js +58 -0
- package/src/utils/helpers.js +89 -0
- package/src/vue/CookieConsent.vue +4 -12
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GeoDetector — detect user's geographic region
|
|
3
|
+
* @module GeoDetector
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { getRegion, requiresConsentBanner } from './RegionRules.js'
|
|
7
|
+
|
|
8
|
+
export class GeoDetector {
|
|
9
|
+
constructor(config = {}) {
|
|
10
|
+
this.config = {
|
|
11
|
+
mode: 'auto', // 'auto' | 'eu-only' | 'always' | 'never'
|
|
12
|
+
|
|
13
|
+
// Detection methods (in order of preference)
|
|
14
|
+
useEdgeHeaders: true, // CF-IPCountry, X-Vercel-IP-Country, etc
|
|
15
|
+
useIPAPI: true, // ipapi.co, ipinfo.io (requires API call)
|
|
16
|
+
useTimezoneFallback: true, // Browser timezone (approximate)
|
|
17
|
+
|
|
18
|
+
// IP API settings
|
|
19
|
+
ipApiUrl: 'https://ipinfo.io/json',
|
|
20
|
+
ipApiTimeout: 3000,
|
|
21
|
+
ipApiToken: null, // Add an API key to increase limits
|
|
22
|
+
|
|
23
|
+
// Backup IP APIs
|
|
24
|
+
backupIpApiUrl: 'https://ipinfo.io/json',
|
|
25
|
+
freeGeoIpUrl: 'https://freegeoip.app/json/',
|
|
26
|
+
ipApiComUrl: 'https://ip-api.com/json/',
|
|
27
|
+
|
|
28
|
+
// Cache settings
|
|
29
|
+
cacheRegion: true,
|
|
30
|
+
cacheKey: 'cmp_user_region',
|
|
31
|
+
cacheDuration: 24 * 60 * 60 * 1000, // 24h for production
|
|
32
|
+
|
|
33
|
+
debug: false,
|
|
34
|
+
...config
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
this.detectedRegion = null
|
|
38
|
+
this.detectionMethod = null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Detect user's region
|
|
43
|
+
* @returns {Promise<string>} Region code (EU, UK, US_CA, ROW, etc)
|
|
44
|
+
*/
|
|
45
|
+
async detect() {
|
|
46
|
+
this._debug('Starting geo detection...')
|
|
47
|
+
|
|
48
|
+
// Check cache first
|
|
49
|
+
if (this.config.cacheRegion) {
|
|
50
|
+
const cached = this._getCachedRegion()
|
|
51
|
+
if (cached) {
|
|
52
|
+
this._debug('Using cached region:', cached)
|
|
53
|
+
this.detectedRegion = cached.region
|
|
54
|
+
this.detectionMethod = 'cache'
|
|
55
|
+
return cached.region
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let countryCode = null
|
|
60
|
+
let stateCode = null
|
|
61
|
+
|
|
62
|
+
// 1. Try edge headers (fastest, most accurate)
|
|
63
|
+
if (this.config.useEdgeHeaders) {
|
|
64
|
+
const edgeResult = this._detectFromEdgeHeaders()
|
|
65
|
+
if (edgeResult) {
|
|
66
|
+
countryCode = edgeResult.country
|
|
67
|
+
stateCode = edgeResult.state
|
|
68
|
+
this.detectionMethod = 'edge-headers'
|
|
69
|
+
this._debug('Detected from edge headers:', countryCode)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 2. Try IP API (requires network request)
|
|
74
|
+
if (!countryCode && this.config.useIPAPI) {
|
|
75
|
+
// Try primary API (ipinfo.io)
|
|
76
|
+
try {
|
|
77
|
+
const ipResult = await this._detectFromIPAPI()
|
|
78
|
+
if (ipResult) {
|
|
79
|
+
countryCode = ipResult.country
|
|
80
|
+
stateCode = ipResult.state
|
|
81
|
+
this.detectionMethod = 'ip-api'
|
|
82
|
+
this._debug('Detected from IP API:', countryCode)
|
|
83
|
+
}
|
|
84
|
+
} catch (error) {
|
|
85
|
+
this._debug('Primary IP API failed:', error)
|
|
86
|
+
|
|
87
|
+
// Try freegeoip.app
|
|
88
|
+
try {
|
|
89
|
+
const freeResult = await this._detectFromFreeGeoIp()
|
|
90
|
+
if (freeResult) {
|
|
91
|
+
countryCode = freeResult.country
|
|
92
|
+
stateCode = freeResult.state
|
|
93
|
+
this.detectionMethod = 'free-geo-ip'
|
|
94
|
+
this._debug('Detected from freegeoip.app:', countryCode)
|
|
95
|
+
}
|
|
96
|
+
} catch (freeError) {
|
|
97
|
+
this._debug('freegeoip.app failed:', freeError)
|
|
98
|
+
|
|
99
|
+
// Try ip-api.com
|
|
100
|
+
try {
|
|
101
|
+
const comResult = await this._detectFromIpApiCom()
|
|
102
|
+
if (comResult) {
|
|
103
|
+
countryCode = comResult.country
|
|
104
|
+
stateCode = comResult.state
|
|
105
|
+
this.detectionMethod = 'ip-api-com'
|
|
106
|
+
this._debug('Detected from ip-api.com:', countryCode)
|
|
107
|
+
}
|
|
108
|
+
} catch (comError) {
|
|
109
|
+
this._debug('ip-api.com also failed:', comError)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 3. Fallback to timezone (approximate)
|
|
116
|
+
if (!countryCode && this.config.useTimezoneFallback) {
|
|
117
|
+
countryCode = this._detectFromTimezone()
|
|
118
|
+
this.detectionMethod = 'timezone-fallback'
|
|
119
|
+
this._debug('Detected from timezone (approximate):', countryCode)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Get region from country code
|
|
123
|
+
const region = countryCode ? getRegion(countryCode, stateCode) : 'ROW'
|
|
124
|
+
this.detectedRegion = region
|
|
125
|
+
|
|
126
|
+
this._debug(`Country: ${countryCode}, State: ${stateCode}, Region: ${region}`)
|
|
127
|
+
|
|
128
|
+
// Cache result
|
|
129
|
+
if (this.config.cacheRegion) {
|
|
130
|
+
this._cacheRegion(region)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
this._debug(`Region detected: ${region} (method: ${this.detectionMethod})`)
|
|
134
|
+
return region
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Check if consent banner should be shown
|
|
139
|
+
* @returns {Promise<boolean>}
|
|
140
|
+
*/
|
|
141
|
+
async shouldShowBanner() {
|
|
142
|
+
const mode = this.config.mode
|
|
143
|
+
|
|
144
|
+
// Mode overrides
|
|
145
|
+
if (mode === 'always') return true
|
|
146
|
+
if (mode === 'never') return false
|
|
147
|
+
|
|
148
|
+
// Detect region
|
|
149
|
+
const region = await this.detect()
|
|
150
|
+
|
|
151
|
+
if (mode === 'eu-only') {
|
|
152
|
+
// Show only for EU/EEA/UK/CH
|
|
153
|
+
return ['EU', 'EEA', 'UK', 'CH'].includes(region)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Auto mode: show if region requires consent
|
|
157
|
+
return requiresConsentBanner(region)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Get detected region (without re-detecting)
|
|
162
|
+
* @returns {string|null}
|
|
163
|
+
*/
|
|
164
|
+
getRegion() {
|
|
165
|
+
return this.detectedRegion
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Get detection method used
|
|
170
|
+
* @returns {string|null}
|
|
171
|
+
*/
|
|
172
|
+
getDetectionMethod() {
|
|
173
|
+
return this.detectionMethod
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Clear cached region
|
|
178
|
+
*/
|
|
179
|
+
clearCache() {
|
|
180
|
+
if (typeof localStorage !== 'undefined') {
|
|
181
|
+
localStorage.removeItem(this.config.cacheKey)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Force refresh region (bypass cache)
|
|
187
|
+
* @returns {Promise<string>}
|
|
188
|
+
*/
|
|
189
|
+
async forceRefresh() {
|
|
190
|
+
this.clearCache()
|
|
191
|
+
this.detectedRegion = null
|
|
192
|
+
this.detectionMethod = null
|
|
193
|
+
return this.detect()
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// --- Detection Methods ---
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Detect from edge/CDN headers
|
|
200
|
+
* @private
|
|
201
|
+
* @returns {Object|null} { country, state }
|
|
202
|
+
*/
|
|
203
|
+
_detectFromEdgeHeaders() {
|
|
204
|
+
// This only works server-side or with edge functions
|
|
205
|
+
// In browser, we can't access these headers directly
|
|
206
|
+
// This is a placeholder for SSR/Edge implementation
|
|
207
|
+
|
|
208
|
+
if (typeof window === 'undefined') {
|
|
209
|
+
// Server-side: check request headers
|
|
210
|
+
// Example: req.headers['cf-ipcountry']
|
|
211
|
+
return null
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Browser: not available
|
|
215
|
+
return null
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Detect from IP API
|
|
220
|
+
* @private
|
|
221
|
+
* @returns {Promise<Object|null>} { country, state }
|
|
222
|
+
*/
|
|
223
|
+
async _detectFromIPAPI() {
|
|
224
|
+
if (typeof fetch === 'undefined') {
|
|
225
|
+
return null
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
const controller = new AbortController()
|
|
230
|
+
const timeoutId = setTimeout(() => controller.abort(), this.config.ipApiTimeout)
|
|
231
|
+
|
|
232
|
+
const url = this.config.ipApiToken
|
|
233
|
+
? `${this.config.ipApiUrl}?token=${this.config.ipApiToken}`
|
|
234
|
+
: this.config.ipApiUrl
|
|
235
|
+
|
|
236
|
+
const response = await fetch(url, {
|
|
237
|
+
signal: controller.signal
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
clearTimeout(timeoutId)
|
|
241
|
+
|
|
242
|
+
if (!response.ok) {
|
|
243
|
+
throw new Error(`IP API returned ${response.status}`)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const data = await response.json()
|
|
247
|
+
|
|
248
|
+
this._debug('IP API response:', data)
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
country: data.country_code || data.country,
|
|
252
|
+
state: data.region_code || this._mapUSState(data.region) // Map full state name to state code
|
|
253
|
+
}
|
|
254
|
+
} catch (error) {
|
|
255
|
+
this._debug('IP API error:', error.message)
|
|
256
|
+
return null
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Map US state names to codes
|
|
262
|
+
* @private
|
|
263
|
+
*/
|
|
264
|
+
_mapUSState(stateName) {
|
|
265
|
+
if (!stateName) return null
|
|
266
|
+
|
|
267
|
+
const stateMap = {
|
|
268
|
+
'Alabama': 'AL', 'Alaska': 'AK', 'Arizona': 'AZ', 'Arkansas': 'AR',
|
|
269
|
+
'California': 'CA', 'Colorado': 'CO', 'Connecticut': 'CT', 'Delaware': 'DE',
|
|
270
|
+
'Florida': 'FL', 'Georgia': 'GA', 'Hawaii': 'HI', 'Idaho': 'ID',
|
|
271
|
+
'Illinois': 'IL', 'Indiana': 'IN', 'Iowa': 'IA', 'Kansas': 'KS',
|
|
272
|
+
'Kentucky': 'KY', 'Louisiana': 'LA', 'Maine': 'ME', 'Maryland': 'MD',
|
|
273
|
+
'Massachusetts': 'MA', 'Michigan': 'MI', 'Minnesota': 'MN', 'Mississippi': 'MS',
|
|
274
|
+
'Missouri': 'MO', 'Montana': 'MT', 'Nebraska': 'NE', 'Nevada': 'NV',
|
|
275
|
+
'New Hampshire': 'NH', 'New Jersey': 'NJ', 'New Mexico': 'NM', 'New York': 'NY',
|
|
276
|
+
'North Carolina': 'NC', 'North Dakota': 'ND', 'Ohio': 'OH', 'Oklahoma': 'OK',
|
|
277
|
+
'Oregon': 'OR', 'Pennsylvania': 'PA', 'Rhode Island': 'RI', 'South Carolina': 'SC',
|
|
278
|
+
'South Dakota': 'SD', 'Tennessee': 'TN', 'Texas': 'TX', 'Utah': 'UT',
|
|
279
|
+
'Vermont': 'VT', 'Virginia': 'VA', 'Washington': 'WA', 'West Virginia': 'WV',
|
|
280
|
+
'Wisconsin': 'WI', 'Wyoming': 'WY'
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return stateMap[stateName] || null
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Detect from freegeoip.app
|
|
288
|
+
* @private
|
|
289
|
+
* @returns {Promise<Object|null>} { country, state }
|
|
290
|
+
*/
|
|
291
|
+
async _detectFromFreeGeoIp() {
|
|
292
|
+
if (typeof fetch === 'undefined') {
|
|
293
|
+
return null
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
const controller = new AbortController()
|
|
298
|
+
const timeoutId = setTimeout(() => controller.abort(), this.config.ipApiTimeout)
|
|
299
|
+
|
|
300
|
+
const response = await fetch(this.config.freeGeoIpUrl, {
|
|
301
|
+
signal: controller.signal
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
clearTimeout(timeoutId)
|
|
305
|
+
|
|
306
|
+
if (!response.ok) {
|
|
307
|
+
throw new Error(`freegeoip.app returned ${response.status}`)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const data = await response.json()
|
|
311
|
+
|
|
312
|
+
this._debug('freegeoip.app response:', data)
|
|
313
|
+
|
|
314
|
+
// freegeoip.app format: {country_code: "US", region_name: "Florida"}
|
|
315
|
+
return {
|
|
316
|
+
country: data.country_code,
|
|
317
|
+
state: this._mapUSState(data.region_name)
|
|
318
|
+
}
|
|
319
|
+
} catch (error) {
|
|
320
|
+
this._debug('freegeoip.app error:', error.message)
|
|
321
|
+
return null
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Detect from ip-api.com
|
|
327
|
+
* @private
|
|
328
|
+
* @returns {Promise<Object|null>} { country, state }
|
|
329
|
+
*/
|
|
330
|
+
async _detectFromIpApiCom() {
|
|
331
|
+
if (typeof fetch === 'undefined') {
|
|
332
|
+
return null
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
try {
|
|
336
|
+
const controller = new AbortController()
|
|
337
|
+
const timeoutId = setTimeout(() => controller.abort(), this.config.ipApiTimeout)
|
|
338
|
+
|
|
339
|
+
const response = await fetch(this.config.ipApiComUrl, {
|
|
340
|
+
signal: controller.signal
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
clearTimeout(timeoutId)
|
|
344
|
+
|
|
345
|
+
if (!response.ok) {
|
|
346
|
+
throw new Error(`ip-api.com returned ${response.status}`)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const data = await response.json()
|
|
350
|
+
|
|
351
|
+
this._debug('ip-api.com response:', data)
|
|
352
|
+
|
|
353
|
+
// ip-api.com format: {countryCode: "US", regionName: "Florida"}
|
|
354
|
+
return {
|
|
355
|
+
country: data.countryCode,
|
|
356
|
+
state: this._mapUSState(data.regionName)
|
|
357
|
+
}
|
|
358
|
+
} catch (error) {
|
|
359
|
+
this._debug('ip-api.com error:', error.message)
|
|
360
|
+
return null
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Detect from backup IP API (ipinfo.io)
|
|
366
|
+
* @private
|
|
367
|
+
* @returns {Promise<Object|null>} { country, state }
|
|
368
|
+
*/
|
|
369
|
+
async _detectFromBackupIPAPI() {
|
|
370
|
+
if (typeof fetch === 'undefined') {
|
|
371
|
+
return null
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
try {
|
|
375
|
+
const controller = new AbortController()
|
|
376
|
+
const timeoutId = setTimeout(() => controller.abort(), this.config.ipApiTimeout)
|
|
377
|
+
|
|
378
|
+
const response = await fetch(this.config.backupIpApiUrl, {
|
|
379
|
+
signal: controller.signal
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
clearTimeout(timeoutId)
|
|
383
|
+
|
|
384
|
+
if (!response.ok) {
|
|
385
|
+
throw new Error(`Backup IP API returned ${response.status}`)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const data = await response.json()
|
|
389
|
+
|
|
390
|
+
this._debug('Backup IP API response:', data)
|
|
391
|
+
|
|
392
|
+
// ipinfo.io format: { country: "US", region: "Florida" }
|
|
393
|
+
return {
|
|
394
|
+
country: data.country,
|
|
395
|
+
state: this._mapUSState(data.region) // Map full state name to state code
|
|
396
|
+
}
|
|
397
|
+
} catch (error) {
|
|
398
|
+
this._debug('Backup IP API error:', error.message)
|
|
399
|
+
return null
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Detect from browser timezone (approximate)
|
|
405
|
+
* @private
|
|
406
|
+
* @returns {string|null} Country code
|
|
407
|
+
*/
|
|
408
|
+
_detectFromTimezone() {
|
|
409
|
+
if (typeof Intl === 'undefined') {
|
|
410
|
+
return null
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
try {
|
|
414
|
+
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
415
|
+
|
|
416
|
+
// Approximate mapping: timezone → country
|
|
417
|
+
const timezoneMap = {
|
|
418
|
+
// Europe
|
|
419
|
+
'Europe/London': 'GB',
|
|
420
|
+
'Europe/Dublin': 'IE',
|
|
421
|
+
'Europe/Paris': 'FR',
|
|
422
|
+
'Europe/Berlin': 'DE',
|
|
423
|
+
'Europe/Rome': 'IT',
|
|
424
|
+
'Europe/Madrid': 'ES',
|
|
425
|
+
'Europe/Amsterdam': 'NL',
|
|
426
|
+
'Europe/Brussels': 'BE',
|
|
427
|
+
'Europe/Vienna': 'AT',
|
|
428
|
+
'Europe/Stockholm': 'SE',
|
|
429
|
+
'Europe/Copenhagen': 'DK',
|
|
430
|
+
'Europe/Helsinki': 'FI',
|
|
431
|
+
'Europe/Oslo': 'NO',
|
|
432
|
+
'Europe/Warsaw': 'PL',
|
|
433
|
+
'Europe/Prague': 'CZ',
|
|
434
|
+
'Europe/Budapest': 'HU',
|
|
435
|
+
'Europe/Bucharest': 'RO',
|
|
436
|
+
'Europe/Athens': 'GR',
|
|
437
|
+
'Europe/Lisbon': 'PT',
|
|
438
|
+
'Europe/Zurich': 'CH',
|
|
439
|
+
|
|
440
|
+
// Americas
|
|
441
|
+
'America/New_York': 'US',
|
|
442
|
+
'America/Chicago': 'US',
|
|
443
|
+
'America/Denver': 'US',
|
|
444
|
+
'America/Los_Angeles': 'US',
|
|
445
|
+
'America/Sao_Paulo': 'BR',
|
|
446
|
+
'America/Mexico_City': 'MX',
|
|
447
|
+
'America/Toronto': 'CA',
|
|
448
|
+
|
|
449
|
+
// Asia
|
|
450
|
+
'Asia/Tokyo': 'JP',
|
|
451
|
+
'Asia/Shanghai': 'CN',
|
|
452
|
+
'Asia/Hong_Kong': 'HK',
|
|
453
|
+
'Asia/Singapore': 'SG',
|
|
454
|
+
'Asia/Dubai': 'AE',
|
|
455
|
+
'Asia/Kolkata': 'IN',
|
|
456
|
+
|
|
457
|
+
// Australia
|
|
458
|
+
'Australia/Sydney': 'AU',
|
|
459
|
+
'Australia/Melbourne': 'AU'
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const countryCode = timezoneMap[timezone]
|
|
463
|
+
|
|
464
|
+
if (countryCode) {
|
|
465
|
+
return countryCode
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Fallback: check timezone prefix
|
|
469
|
+
if (timezone.startsWith('Europe/')) {
|
|
470
|
+
// Return a representative EU country instead of 'EU'
|
|
471
|
+
// This ensures getRegion() works correctly
|
|
472
|
+
return 'DE' // Germany as default EU country
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return null
|
|
476
|
+
} catch (error) {
|
|
477
|
+
this._debug('Timezone detection error:', error)
|
|
478
|
+
return null
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Get cached region
|
|
484
|
+
* @private
|
|
485
|
+
*/
|
|
486
|
+
_getCachedRegion() {
|
|
487
|
+
if (typeof localStorage === 'undefined') {
|
|
488
|
+
return null
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
try {
|
|
492
|
+
const cached = localStorage.getItem(this.config.cacheKey)
|
|
493
|
+
if (!cached) return null
|
|
494
|
+
|
|
495
|
+
const data = JSON.parse(cached)
|
|
496
|
+
const age = Date.now() - data.timestamp
|
|
497
|
+
|
|
498
|
+
if (age > this.config.cacheDuration) {
|
|
499
|
+
// Cache expired
|
|
500
|
+
localStorage.removeItem(this.config.cacheKey)
|
|
501
|
+
return null
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
return data
|
|
505
|
+
} catch (error) {
|
|
506
|
+
return null
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Cache region
|
|
512
|
+
* @private
|
|
513
|
+
*/
|
|
514
|
+
_cacheRegion(region) {
|
|
515
|
+
if (typeof localStorage === 'undefined') {
|
|
516
|
+
return
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
try {
|
|
520
|
+
const data = {
|
|
521
|
+
region,
|
|
522
|
+
timestamp: Date.now(),
|
|
523
|
+
method: this.detectionMethod
|
|
524
|
+
}
|
|
525
|
+
localStorage.setItem(this.config.cacheKey, JSON.stringify(data))
|
|
526
|
+
} catch (error) {
|
|
527
|
+
this._debug('Failed to cache region:', error)
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
_debug(...args) {
|
|
532
|
+
if (this.config.debug) {
|
|
533
|
+
console.log('[GeoDetector]', ...args)
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Region Rules — GDPR/CCPA region definitions
|
|
3
|
+
* @module RegionRules
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const REGION_RULES = {
|
|
7
|
+
// European Union (GDPR)
|
|
8
|
+
EU: {
|
|
9
|
+
name: 'European Union',
|
|
10
|
+
countries: [
|
|
11
|
+
'AT', 'BE', 'BG', 'HR', 'CY', 'CZ', 'DK', 'EE', 'FI', 'FR',
|
|
12
|
+
'DE', 'GR', 'HU', 'IE', 'IT', 'LV', 'LT', 'LU', 'MT', 'NL',
|
|
13
|
+
'PL', 'PT', 'RO', 'SK', 'SI', 'ES', 'SE', 'MD'
|
|
14
|
+
],
|
|
15
|
+
regulation: 'GDPR',
|
|
16
|
+
requiresConsent: true,
|
|
17
|
+
defaultMode: 'opt-in'
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
// European Economic Area (GDPR)
|
|
21
|
+
EEA: {
|
|
22
|
+
name: 'European Economic Area',
|
|
23
|
+
countries: ['IS', 'LI', 'NO'],
|
|
24
|
+
regulation: 'GDPR',
|
|
25
|
+
requiresConsent: true,
|
|
26
|
+
defaultMode: 'opt-in'
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
// United Kingdom (UK GDPR)
|
|
30
|
+
UK: {
|
|
31
|
+
name: 'United Kingdom',
|
|
32
|
+
countries: ['GB'],
|
|
33
|
+
regulation: 'UK GDPR',
|
|
34
|
+
requiresConsent: true,
|
|
35
|
+
defaultMode: 'opt-in'
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
// Switzerland (FADP)
|
|
39
|
+
CH: {
|
|
40
|
+
name: 'Switzerland',
|
|
41
|
+
countries: ['CH'],
|
|
42
|
+
regulation: 'FADP',
|
|
43
|
+
requiresConsent: true,
|
|
44
|
+
defaultMode: 'opt-in'
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
// United States (Federal)
|
|
48
|
+
US: {
|
|
49
|
+
name: 'United States',
|
|
50
|
+
countries: ['US'],
|
|
51
|
+
regulation: 'US Privacy Laws',
|
|
52
|
+
requiresConsent: true,
|
|
53
|
+
defaultMode: 'opt-out'
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
// Brazil (LGPD)
|
|
57
|
+
BR: {
|
|
58
|
+
name: 'Brazil',
|
|
59
|
+
countries: ['BR'],
|
|
60
|
+
regulation: 'LGPD',
|
|
61
|
+
requiresConsent: true,
|
|
62
|
+
defaultMode: 'opt-in'
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
// Canada (PIPEDA)
|
|
66
|
+
CA: {
|
|
67
|
+
name: 'Canada',
|
|
68
|
+
countries: ['CA'],
|
|
69
|
+
regulation: 'PIPEDA',
|
|
70
|
+
requiresConsent: true,
|
|
71
|
+
defaultMode: 'opt-in'
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
// Rest of World
|
|
75
|
+
ROW: {
|
|
76
|
+
name: 'Rest of World',
|
|
77
|
+
countries: [],
|
|
78
|
+
regulation: null,
|
|
79
|
+
requiresConsent: true, // Show essential modal
|
|
80
|
+
defaultMode: 'opt-out',
|
|
81
|
+
essentialOnly: true // Only necessary cookies
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function isEU(countryCode) {
|
|
86
|
+
return REGION_RULES.EU.countries.includes(countryCode)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function isEEA(countryCode) {
|
|
90
|
+
return REGION_RULES.EEA.countries.includes(countryCode)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function isUK(countryCode) {
|
|
94
|
+
return countryCode === 'GB'
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function requiresGDPR(countryCode) {
|
|
98
|
+
return isEU(countryCode) || isEEA(countryCode) || isUK(countryCode) || countryCode === 'CH'
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function getRegion(countryCode, stateCode = null) {
|
|
102
|
+
if (isEU(countryCode)) return 'EU'
|
|
103
|
+
if (isEEA(countryCode)) return 'EEA'
|
|
104
|
+
if (isUK(countryCode)) return 'UK'
|
|
105
|
+
if (countryCode === 'CH') return 'CH'
|
|
106
|
+
if (countryCode === 'BR') return 'BR'
|
|
107
|
+
if (countryCode === 'CA') return 'CA'
|
|
108
|
+
if (countryCode === 'US') return 'US' // All US states now use US region
|
|
109
|
+
|
|
110
|
+
return 'ROW'
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function requiresConsentBanner(region) {
|
|
114
|
+
const rule = REGION_RULES[region]
|
|
115
|
+
return rule ? rule.requiresConsent : false
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function getRegulation(region) {
|
|
119
|
+
const rule = REGION_RULES[region]
|
|
120
|
+
return rule ? rule.regulation : null
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function isEssentialOnly(region) {
|
|
124
|
+
const rule = REGION_RULES[region]
|
|
125
|
+
return rule ? rule.essentialOnly || false : false
|
|
126
|
+
}
|
package/src/geo/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Geo Detection System — detect user's geographic region for GDPR/CCPA compliance
|
|
3
|
+
* @module geo
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { GeoDetector } from './GeoDetector.js'
|
|
7
|
+
export {
|
|
8
|
+
REGION_RULES,
|
|
9
|
+
isEU,
|
|
10
|
+
isEEA,
|
|
11
|
+
isUK,
|
|
12
|
+
requiresGDPR,
|
|
13
|
+
getRegion,
|
|
14
|
+
requiresConsentBanner,
|
|
15
|
+
getRegulation
|
|
16
|
+
} from './RegionRules.js'
|