@owlmeans/consent 0.1.18-rc.9 → 0.1.18

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 (115) hide show
  1. package/README.md +229 -0
  2. package/build/consts.d.ts +78 -3
  3. package/build/consts.d.ts.map +1 -1
  4. package/build/consts.js +104 -0
  5. package/build/consts.js.map +1 -1
  6. package/build/consts.local.d.ts +12 -0
  7. package/build/consts.local.d.ts.map +1 -0
  8. package/build/consts.local.js +12 -0
  9. package/build/consts.local.js.map +1 -0
  10. package/build/geo/types.d.ts +47 -0
  11. package/build/geo/types.d.ts.map +1 -0
  12. package/build/geo/types.js +2 -0
  13. package/build/geo/types.js.map +1 -0
  14. package/build/geo.d.ts +4 -0
  15. package/build/geo.d.ts.map +1 -0
  16. package/build/geo.js +96 -0
  17. package/build/geo.js.map +1 -0
  18. package/build/gtobserver/types.d.ts +75 -0
  19. package/build/gtobserver/types.d.ts.map +1 -0
  20. package/build/gtobserver/types.js +2 -0
  21. package/build/gtobserver/types.js.map +1 -0
  22. package/build/gtobserver.d.ts +7 -0
  23. package/build/gtobserver.d.ts.map +1 -0
  24. package/build/gtobserver.js +142 -0
  25. package/build/gtobserver.js.map +1 -0
  26. package/build/i18n/be.json +8 -1
  27. package/build/i18n/de.json +8 -1
  28. package/build/i18n/en.json +8 -1
  29. package/build/i18n/es.json +8 -1
  30. package/build/i18n/fr.json +8 -1
  31. package/build/i18n/pl.json +8 -1
  32. package/build/i18n/ru.json +8 -1
  33. package/build/i18n/types.d.ts +11 -0
  34. package/build/i18n/types.d.ts.map +1 -0
  35. package/build/i18n/types.js +2 -0
  36. package/build/i18n/types.js.map +1 -0
  37. package/build/i18n/uk.json +8 -1
  38. package/build/i18n.d.ts +4 -14
  39. package/build/i18n.d.ts.map +1 -1
  40. package/build/i18n.js +12 -27
  41. package/build/i18n.js.map +1 -1
  42. package/build/index.d.ts +13 -5
  43. package/build/index.d.ts.map +1 -1
  44. package/build/index.js +5 -3
  45. package/build/index.js.map +1 -1
  46. package/build/linker/types.d.ts +59 -0
  47. package/build/linker/types.d.ts.map +1 -0
  48. package/build/linker/types.js +2 -0
  49. package/build/linker/types.js.map +1 -0
  50. package/build/linker.d.ts +5 -74
  51. package/build/linker.d.ts.map +1 -1
  52. package/build/linker.js +290 -324
  53. package/build/linker.js.map +1 -1
  54. package/build/plugins/types.d.ts +42 -0
  55. package/build/plugins/types.d.ts.map +1 -0
  56. package/build/plugins/types.js +2 -0
  57. package/build/plugins/types.js.map +1 -0
  58. package/build/plugins.d.ts +4 -68
  59. package/build/plugins.d.ts.map +1 -1
  60. package/build/plugins.js +73 -61
  61. package/build/plugins.js.map +1 -1
  62. package/build/storage/types.d.ts +31 -0
  63. package/build/storage/types.d.ts.map +1 -0
  64. package/build/storage/types.js +2 -0
  65. package/build/storage/types.js.map +1 -0
  66. package/build/storage.d.ts +6 -22
  67. package/build/storage.d.ts.map +1 -1
  68. package/build/storage.js +72 -81
  69. package/build/storage.js.map +1 -1
  70. package/build/store.d.ts +2 -2
  71. package/build/store.d.ts.map +1 -1
  72. package/build/store.js +115 -31
  73. package/build/store.js.map +1 -1
  74. package/build/types.d.ts +141 -5
  75. package/build/types.d.ts.map +1 -1
  76. package/build/types.local.d.ts +9 -0
  77. package/build/types.local.d.ts.map +1 -0
  78. package/build/types.local.js +2 -0
  79. package/build/types.local.js.map +1 -0
  80. package/package.json +3 -3
  81. package/src/consts.local.ts +16 -0
  82. package/src/consts.ts +120 -3
  83. package/src/geo/types.ts +49 -0
  84. package/src/geo.ts +122 -0
  85. package/src/gtobserver/types.ts +73 -0
  86. package/src/gtobserver.ts +182 -0
  87. package/src/i18n/be.json +8 -1
  88. package/src/i18n/de.json +8 -1
  89. package/src/i18n/en.json +8 -1
  90. package/src/i18n/es.json +8 -1
  91. package/src/i18n/fr.json +8 -1
  92. package/src/i18n/pl.json +8 -1
  93. package/src/i18n/ru.json +8 -1
  94. package/src/i18n/types.ts +11 -0
  95. package/src/i18n/uk.json +8 -1
  96. package/src/i18n.ts +20 -32
  97. package/src/index.ts +13 -12
  98. package/src/linker/types.ts +59 -0
  99. package/src/linker.ts +301 -349
  100. package/src/plugins/types.ts +42 -0
  101. package/src/plugins.ts +82 -105
  102. package/src/storage/types.ts +31 -0
  103. package/src/storage.ts +75 -77
  104. package/src/store.ts +125 -31
  105. package/src/types.local.ts +9 -0
  106. package/src/types.ts +148 -5
  107. package/tests/consent.spec.ts +26 -28
  108. package/tests/geo.spec.ts +474 -0
  109. package/tests/linker.spec.ts +68 -74
  110. package/tests/tracking.spec.ts +14 -14
  111. package/build/gtm.d.ts +0 -72
  112. package/build/gtm.d.ts.map +0 -1
  113. package/build/gtm.js +0 -182
  114. package/build/gtm.js.map +0 -1
  115. package/src/gtm.ts +0 -220
@@ -1,16 +1,10 @@
1
1
  import { describe, test, expect, beforeEach, afterEach } from 'bun:test'
2
- import {
3
- CONSENT_ANALYTICS, CONSENT_ESSENTIAL, CONSENT_KEY, CONSENT_LANGUAGE_KEY, CONSENT_MARKETING,
4
- DEFAULT_CONSENT_CATEGORIES,
5
- } from '../src/consts.js'
6
- import { consentBootstrapScript } from '../src/gtm.js'
7
- import {
8
- consentLinker, consentLinkerScript, decodeConsentLink, encodeConsentLink, stripConsentLinkParam, writeConsentLanguage,
9
- CONSENT_LINK_MAX_AGE, CONSENT_LINK_PARAM,
10
- } from '../src/linker.js'
11
- import { adoptConsentLanguage, consentDomains, decorateConsentUrl, registerConsentPlugin } from '../src/plugins.js'
2
+ import { CONSENT_ANALYTICS, CONSENT_ESSENTIAL, CONSENT_KEY, CONSENT_LANGUAGE_KEY, CONSENT_MARKETING, DEFAULT_CONSENT_CATEGORIES, CONSENT_LINK_MAX_AGE, CONSENT_LINK_PARAM } from '../src/consts.js'
3
+ import { consentModeHelper } from '../src/gtobserver.js'
4
+ import { consentLinkHelper } from '../src/linker.js'
5
+ import { consentPluginHelper } from '../src/plugins.js'
12
6
  import { makeConsentStore } from '../src/store.js'
13
- import { readConsent, writeConsent } from '../src/storage.js'
7
+ import { consentStorageHelper } from '../src/storage.js'
14
8
  import type { ConsentRecord } from '../src/types.js'
15
9
 
16
10
  /** A minimal browser: storage, a settable referrer, and a `location`/`history` pair that actually
@@ -78,8 +72,8 @@ beforeEach(() => {
78
72
  describe('encodeConsentLink / decodeConsentLink', () => {
79
73
  test('round-trips a record through the optional categories only', () => {
80
74
  const record: ConsentRecord = { [CONSENT_ESSENTIAL]: true, [CONSENT_ANALYTICS]: true, [CONSENT_MARKETING]: false }
81
- const encoded = encodeConsentLink(record)
82
- const decoded = decodeConsentLink(encoded)
75
+ const encoded = consentLinkHelper.encodeConsentLink(record)
76
+ const decoded = consentLinkHelper.decodeConsentLink(encoded)
83
77
 
84
78
  expect(decoded?.v).toBe(2)
85
79
  expect(decoded?.c).toEqual({ [CONSENT_ANALYTICS]: 1, [CONSENT_MARKETING]: 0 })
@@ -87,26 +81,26 @@ describe('encodeConsentLink / decodeConsentLink', () => {
87
81
  })
88
82
 
89
83
  test('rejects garbage rather than throwing', () => {
90
- expect(decodeConsentLink('not-base64url-json')).toBeNull()
91
- expect(decodeConsentLink('')).toBeNull()
84
+ expect(consentLinkHelper.decodeConsentLink('not-base64url-json')).toBeNull()
85
+ expect(consentLinkHelper.decodeConsentLink('')).toBeNull()
92
86
  })
93
87
 
94
88
  test('rejects a payload of the wrong version', () => {
95
89
  const bad = Buffer.from(JSON.stringify({ v: 1, c: {}, t: nowSeconds() })).toString('base64url')
96
90
 
97
- expect(decodeConsentLink(bad)).toBeNull()
91
+ expect(consentLinkHelper.decodeConsentLink(bad)).toBeNull()
98
92
  })
99
93
  })
100
94
 
101
95
  describe('consentLinker().decorate', () => {
102
- const plugin = consentLinker()
96
+ const plugin = consentLinkHelper.consentLinker()
103
97
  const record: ConsentRecord = { [CONSENT_ESSENTIAL]: true, [CONSENT_ANALYTICS]: true, [CONSENT_MARKETING]: false }
104
98
 
105
99
  test('decorates a listed, foreign host', () => {
106
100
  const decorated = plugin.decorate!(new URL('https://site.test/legal/cookies'), record, linkerOpts)
107
101
 
108
102
  expect(decorated).not.toBeNull()
109
- const payload = decodeConsentLink(decorated!.searchParams.get(CONSENT_LINK_PARAM)!)
103
+ const payload = consentLinkHelper.decodeConsentLink(decorated!.searchParams.get(CONSENT_LINK_PARAM)!)
110
104
  expect(payload?.c[CONSENT_ANALYTICS]).toBe(1)
111
105
  })
112
106
 
@@ -132,8 +126,8 @@ describe('consentLinker().decorate', () => {
132
126
  })
133
127
 
134
128
  describe('consentLinker().adopt — every trust rule', () => {
135
- const plugin = consentLinker()
136
- const valid = () => encodeConsentLink({ [CONSENT_ANALYTICS]: true, [CONSENT_MARKETING]: false })
129
+ const plugin = consentLinkHelper.consentLinker()
130
+ const valid = () => consentLinkHelper.encodeConsentLink({ [CONSENT_ANALYTICS]: true, [CONSENT_MARKETING]: false })
137
131
 
138
132
  test('adopts a fresh, referred, fully-covered decision', () => {
139
133
  env.goto(`https://platform.test/dispatcher?owlcc=${valid()}`, 'https://site.test/')
@@ -199,21 +193,21 @@ describe('consentLinker().adopt — every trust rule', () => {
199
193
  describe('stripConsentLinkParam', () => {
200
194
  test('removes only the parameter, keeping the rest and the hash', () => {
201
195
  env.goto('https://platform.test/dispatcher?owlcc=abc&next=%2Fhome&ref=site#panel')
202
- stripConsentLinkParam(linkerOpts)
196
+ consentLinkHelper.stripConsentLinkParam(linkerOpts)
203
197
 
204
198
  expect(env.href()).toBe('https://platform.test/dispatcher?next=%2Fhome&ref=site#panel')
205
199
  })
206
200
 
207
201
  test('is a no-op with no parameter present', () => {
208
202
  env.goto('https://platform.test/dispatcher?next=%2Fhome')
209
- stripConsentLinkParam(linkerOpts)
203
+ consentLinkHelper.stripConsentLinkParam(linkerOpts)
210
204
 
211
205
  expect(env.href()).toBe('https://platform.test/dispatcher?next=%2Fhome')
212
206
  })
213
207
 
214
208
  test('is a no-op with no linker configured', () => {
215
209
  env.goto('https://platform.test/dispatcher?owlcc=abc')
216
- stripConsentLinkParam({})
210
+ consentLinkHelper.stripConsentLinkParam({})
217
211
 
218
212
  expect(env.href()).toBe('https://platform.test/dispatcher?owlcc=abc')
219
213
  })
@@ -221,8 +215,8 @@ describe('stripConsentLinkParam', () => {
221
215
 
222
216
  describe('consentDomains / decorateConsentUrl (registry level)', () => {
223
217
  test('discloses the current host plus every plugin-named domain, deduplicated', () => {
224
- registerConsentPlugin(consentLinker())
225
- const domains = consentDomains(linkerOpts)
218
+ consentPluginHelper.registerConsentPlugin(consentLinkHelper.consentLinker())
219
+ const domains = consentPluginHelper.consentDomains(linkerOpts)
226
220
 
227
221
  expect(domains).toContain('platform.test')
228
222
  expect(domains).toContain('site.test')
@@ -230,18 +224,18 @@ describe('consentDomains / decorateConsentUrl (registry level)', () => {
230
224
  })
231
225
 
232
226
  test('decorateConsentUrl applies the registered plugin to a plain URL', () => {
233
- registerConsentPlugin(consentLinker())
227
+ consentPluginHelper.registerConsentPlugin(consentLinkHelper.consentLinker())
234
228
  const record: ConsentRecord = { [CONSENT_ESSENTIAL]: true, [CONSENT_ANALYTICS]: false, [CONSENT_MARKETING]: false }
235
- const decorated = decorateConsentUrl('https://site.test/legal/cookies', record, linkerOpts)
229
+ const decorated = consentPluginHelper.decorateConsentUrl('https://site.test/legal/cookies', record, linkerOpts)
236
230
 
237
231
  expect(decorated.searchParams.has(CONSENT_LINK_PARAM)).toBe(true)
238
232
  })
239
233
 
240
234
  test('registering the same alias twice replaces rather than duplicates', () => {
241
- registerConsentPlugin(consentLinker())
242
- registerConsentPlugin(consentLinker())
235
+ consentPluginHelper.registerConsentPlugin(consentLinkHelper.consentLinker())
236
+ consentPluginHelper.registerConsentPlugin(consentLinkHelper.consentLinker())
243
237
  const record: ConsentRecord = { [CONSENT_ESSENTIAL]: true }
244
- const decorated = decorateConsentUrl('https://site.test/', record, linkerOpts)
238
+ const decorated = consentPluginHelper.decorateConsentUrl('https://site.test/', record, linkerOpts)
245
239
 
246
240
  // A duplicate `decorate` implementation would still be idempotent (same value both times), so
247
241
  // what this actually guards is that the registry never grows unbounded per alias.
@@ -252,7 +246,7 @@ describe('consentDomains / decorateConsentUrl (registry level)', () => {
252
246
  describe('the store, with a linker configured', () => {
253
247
  test('adopts on init when nothing is stored yet, and applies it', () => {
254
248
  const record = { [CONSENT_ANALYTICS]: true, [CONSENT_MARKETING]: false }
255
- env.goto(`https://platform.test/dispatcher?owlcc=${encodeConsentLink(record)}`, 'https://site.test/')
249
+ env.goto(`https://platform.test/dispatcher?owlcc=${consentLinkHelper.encodeConsentLink(record)}`, 'https://site.test/')
256
250
 
257
251
  const store = makeConsentStore()
258
252
  store.init({ silent: true, linker: { domains: DOMAINS } })
@@ -261,7 +255,7 @@ describe('the store, with a linker configured', () => {
261
255
  expect(store.get().record?.[CONSENT_ANALYTICS]).toBe(true)
262
256
  expect(store.granted(CONSENT_ESSENTIAL)).toBe(true)
263
257
  // Written through, not just held in memory — a second page load must see it too.
264
- expect(readConsent()?.[CONSENT_ANALYTICS]).toBe(true)
258
+ expect(consentStorageHelper.readConsent()?.[CONSENT_ANALYTICS]).toBe(true)
265
259
  })
266
260
 
267
261
  test('strips the parameter from the URL either way', () => {
@@ -274,9 +268,9 @@ describe('the store, with a linker configured', () => {
274
268
  })
275
269
 
276
270
  test('never overwrites a decision this document already made', () => {
277
- writeConsent({ [CONSENT_ANALYTICS]: false, [CONSENT_MARKETING]: false })
271
+ consentStorageHelper.writeConsent({ [CONSENT_ANALYTICS]: false, [CONSENT_MARKETING]: false })
278
272
  const incoming = { [CONSENT_ANALYTICS]: true, [CONSENT_MARKETING]: true }
279
- env.goto(`https://platform.test/dispatcher?owlcc=${encodeConsentLink(incoming)}`, 'https://site.test/')
273
+ env.goto(`https://platform.test/dispatcher?owlcc=${consentLinkHelper.encodeConsentLink(incoming)}`, 'https://site.test/')
280
274
 
281
275
  const store = makeConsentStore()
282
276
  store.init({ silent: true, linker: { domains: DOMAINS } })
@@ -297,7 +291,7 @@ describe('the store, with a linker configured', () => {
297
291
 
298
292
  describe('consentBootstrapScript with a linker: order and shape', () => {
299
293
  test('the linker fragment sits after the default push and before the storage read', () => {
300
- const script = consentBootstrapScript({ linker: { domains: DOMAINS } })
294
+ const script = consentModeHelper.consentBootstrapScript({ linker: { domains: DOMAINS } })
301
295
  const defaultAt = script.indexOf("'consent','default'")
302
296
  const linkerAt = script.indexOf('URLSearchParams')
303
297
  const storageAt = script.indexOf('w.localStorage.getItem')
@@ -308,11 +302,11 @@ describe('consentBootstrapScript with a linker: order and shape', () => {
308
302
  })
309
303
 
310
304
  test('emits nothing extra with no linker configured', () => {
311
- expect(consentBootstrapScript()).not.toContain('URLSearchParams')
305
+ expect(consentModeHelper.consentBootstrapScript()).not.toContain('URLSearchParams')
312
306
  })
313
307
 
314
308
  test('escapes "<" inside embedded strings', () => {
315
- const script = consentLinkerScript({ linker: { domains: ['<script>evil.test'] } })
309
+ const script = consentLinkHelper.consentLinkerScript({ linker: { domains: ['<script>evil.test'] } })
316
310
 
317
311
  expect(script).not.toContain('<script>evil.test')
318
312
  expect(script).toContain('\\u003cscript>evil.test')
@@ -322,31 +316,31 @@ describe('consentBootstrapScript with a linker: order and shape', () => {
322
316
  describe('consentLinkerScript, executed', () => {
323
317
  test('adopts a valid link and strips the parameter, via new Function', () => {
324
318
  const record = { [CONSENT_ANALYTICS]: true, [CONSENT_MARKETING]: false }
325
- env.goto(`https://platform.test/dispatcher?owlcc=${encodeConsentLink(record)}`, 'https://site.test/')
319
+ env.goto(`https://platform.test/dispatcher?owlcc=${consentLinkHelper.encodeConsentLink(record)}`, 'https://site.test/')
326
320
 
327
- const script = consentLinkerScript({ linker: { domains: DOMAINS } })
321
+ const script = consentLinkHelper.consentLinkerScript({ linker: { domains: DOMAINS } })
328
322
  // eslint-disable-next-line no-new-func
329
323
  new Function(script)()
330
324
 
331
325
  expect(env.href()).toBe('https://platform.test/dispatcher')
332
- expect(readConsent()?.[CONSENT_ANALYTICS]).toBe(true)
326
+ expect(consentStorageHelper.readConsent()?.[CONSENT_ANALYTICS]).toBe(true)
333
327
  })
334
328
 
335
329
  test('never overwrites an existing record, and still strips', () => {
336
- writeConsent({ [CONSENT_ANALYTICS]: false, [CONSENT_MARKETING]: false })
330
+ consentStorageHelper.writeConsent({ [CONSENT_ANALYTICS]: false, [CONSENT_MARKETING]: false })
337
331
  const incoming = { [CONSENT_ANALYTICS]: true, [CONSENT_MARKETING]: true }
338
- env.goto(`https://platform.test/dispatcher?owlcc=${encodeConsentLink(incoming)}`, 'https://site.test/')
332
+ env.goto(`https://platform.test/dispatcher?owlcc=${consentLinkHelper.encodeConsentLink(incoming)}`, 'https://site.test/')
339
333
 
340
- const script = consentLinkerScript({ linker: { domains: DOMAINS } })
334
+ const script = consentLinkHelper.consentLinkerScript({ linker: { domains: DOMAINS } })
341
335
  // eslint-disable-next-line no-new-func
342
336
  new Function(script)()
343
337
 
344
338
  expect(env.href()).toBe('https://platform.test/dispatcher')
345
- expect(readConsent()?.[CONSENT_ANALYTICS]).toBe(false)
339
+ expect(consentStorageHelper.readConsent()?.[CONSENT_ANALYTICS]).toBe(false)
346
340
  })
347
341
 
348
342
  test('is an empty string with no linker configured', () => {
349
- expect(consentLinkerScript({})).toBe('')
343
+ expect(consentLinkHelper.consentLinkerScript({})).toBe('')
350
344
  })
351
345
  })
352
346
 
@@ -370,7 +364,7 @@ describe('language: the link parameter', () => {
370
364
 
371
365
  test('carries the page language as `l`, lower-cased, when the sender turned it on', () => {
372
366
  env.lang('PL')
373
- const decoded = decodeConsentLink(encodeConsentLink({ [CONSENT_ANALYTICS]: true }, withLanguage))
367
+ const decoded = consentLinkHelper.decodeConsentLink(consentLinkHelper.encodeConsentLink({ [CONSENT_ANALYTICS]: true }, withLanguage))
374
368
 
375
369
  expect(decoded?.l).toBe('pl')
376
370
  expect(decoded?.c).toEqual({ [CONSENT_ANALYTICS]: 1, [CONSENT_MARKETING]: 0 })
@@ -379,20 +373,20 @@ describe('language: the link parameter', () => {
379
373
  test('carries no `l` when the sender did not turn it on, whatever the page says', () => {
380
374
  env.lang('pl')
381
375
 
382
- expect(decodeConsentLink(encodeConsentLink({ [CONSENT_ANALYTICS]: true }, linkerOpts))?.l).toBeUndefined()
376
+ expect(consentLinkHelper.decodeConsentLink(consentLinkHelper.encodeConsentLink({ [CONSENT_ANALYTICS]: true }, linkerOpts))?.l).toBeUndefined()
383
377
  })
384
378
 
385
379
  test('carries no `l` for a page with no usable language', () => {
386
380
  for (const lang of ['', 'not a language!', '12']) {
387
381
  env.lang(lang)
388
382
 
389
- expect(decodeConsentLink(encodeConsentLink(null, withLanguage))?.l).toBeUndefined()
383
+ expect(consentLinkHelper.decodeConsentLink(consentLinkHelper.encodeConsentLink(null, withLanguage))?.l).toBeUndefined()
390
384
  }
391
385
  })
392
386
 
393
387
  test('encodes no decision at all as an empty `c`', () => {
394
388
  env.lang('de')
395
- const decoded = decodeConsentLink(encodeConsentLink(null, withLanguage))
389
+ const decoded = consentLinkHelper.decodeConsentLink(consentLinkHelper.encodeConsentLink(null, withLanguage))
396
390
 
397
391
  expect(decoded?.c).toEqual({})
398
392
  expect(decoded?.l).toBe('de')
@@ -402,7 +396,7 @@ describe('language: the link parameter', () => {
402
396
  const raw = Buffer.from(JSON.stringify({
403
397
  v: 2, c: { [CONSENT_ANALYTICS]: 1 }, t: nowSeconds(), l: '../../etc',
404
398
  })).toString('base64url')
405
- const decoded = decodeConsentLink(raw)
399
+ const decoded = consentLinkHelper.decodeConsentLink(raw)
406
400
 
407
401
  expect(decoded).not.toBeNull()
408
402
  expect(decoded?.l).toBeUndefined()
@@ -411,7 +405,7 @@ describe('language: the link parameter', () => {
411
405
  })
412
406
 
413
407
  describe('language: consentLinker().decorate', () => {
414
- const plugin = consentLinker()
408
+ const plugin = consentLinkHelper.consentLinker()
415
409
  const withLanguage = { linker: { domains: DOMAINS, language: {} } }
416
410
  const record: ConsentRecord = { [CONSENT_ESSENTIAL]: true, [CONSENT_ANALYTICS]: true, [CONSENT_MARKETING]: false }
417
411
 
@@ -420,14 +414,14 @@ describe('language: consentLinker().decorate', () => {
420
414
  const decorated = plugin.decorate!(new URL('https://site.test/start'), null, withLanguage)
421
415
 
422
416
  expect(decorated).not.toBeNull()
423
- const payload = decodeConsentLink(decorated!.searchParams.get(CONSENT_LINK_PARAM)!)
417
+ const payload = consentLinkHelper.decodeConsentLink(decorated!.searchParams.get(CONSENT_LINK_PARAM)!)
424
418
  expect(payload?.l).toBe('uk')
425
419
  expect(payload?.c).toEqual({})
426
420
  })
427
421
 
428
422
  test('carries the decision and the language together', () => {
429
423
  env.lang('pl')
430
- const payload = decodeConsentLink(
424
+ const payload = consentLinkHelper.decodeConsentLink(
431
425
  plugin.decorate!(new URL('https://site.test/start'), record, withLanguage)!.searchParams.get(CONSENT_LINK_PARAM)!
432
426
  )
433
427
 
@@ -455,12 +449,12 @@ describe('language: consentLinker().decorate', () => {
455
449
  })
456
450
 
457
451
  describe('language: consentLinker().adoptLanguage', () => {
458
- const plugin = consentLinker()
452
+ const plugin = consentLinkHelper.consentLinker()
459
453
  const receiving = { linker: { domains: DOMAINS, language: { supported: ['en', 'pl', 'de', 'fr'] } } }
460
454
  const carried = (lang: string, record: ConsentRecord | null = null): string => {
461
455
  env.lang(lang)
462
456
 
463
- return encodeConsentLink(record, { linker: { domains: DOMAINS, language: {} } })
457
+ return consentLinkHelper.encodeConsentLink(record, { linker: { domains: DOMAINS, language: {} } })
464
458
  }
465
459
  const arrive = (owlcc: string, referrer = 'https://site.test/') => {
466
460
  env.goto(`https://platform.test/dispatcher?owlcc=${owlcc}`, referrer)
@@ -526,11 +520,11 @@ describe('language: consentLinker().adoptLanguage', () => {
526
520
  })
527
521
 
528
522
  test('registry: adoptConsentLanguage asks the registered plugin', () => {
529
- registerConsentPlugin(consentLinker())
523
+ consentPluginHelper.registerConsentPlugin(consentLinkHelper.consentLinker())
530
524
  arrive(carried('pl'))
531
525
 
532
- expect(adoptConsentLanguage(receiving)).toBe('pl')
533
- expect(adoptConsentLanguage(linkerOpts)).toBeNull()
526
+ expect(consentPluginHelper.adoptConsentLanguage(receiving)).toBe('pl')
527
+ expect(consentPluginHelper.adoptConsentLanguage(linkerOpts)).toBeNull()
534
528
  })
535
529
  })
536
530
 
@@ -538,7 +532,7 @@ describe('language: strictly necessary, stored whatever the cookie decision is',
538
532
  const receiving = { silent: true, linker: { domains: DOMAINS, language: { supported: ['en', 'pl', 'de'] } } }
539
533
  const arrive = (lang: string, record: ConsentRecord | null = null, referrer = 'https://site.test/') => {
540
534
  env.lang(lang)
541
- const owlcc = encodeConsentLink(record, { linker: { domains: DOMAINS, language: {} } })
535
+ const owlcc = consentLinkHelper.encodeConsentLink(record, { linker: { domains: DOMAINS, language: {} } })
542
536
  env.goto(`https://platform.test/dispatcher?owlcc=${owlcc}`, referrer)
543
537
  env.lang('en')
544
538
  }
@@ -551,9 +545,9 @@ describe('language: strictly necessary, stored whatever the cookie decision is',
551
545
  for (const record of [null, refused, accepted, withRetiredKey]) {
552
546
  env.reset()
553
547
  if (record != null) {
554
- writeConsent(record)
548
+ consentStorageHelper.writeConsent(record)
555
549
  }
556
- expect(writeConsentLanguage('pl', receiving)).toBe(true)
550
+ expect(consentLinkHelper.writeConsentLanguage('pl', receiving)).toBe(true)
557
551
  expect(env.store.get(CONSENT_LANGUAGE_KEY)).toBe('pl')
558
552
  }
559
553
  })
@@ -563,7 +557,7 @@ describe('language: strictly necessary, stored whatever the cookie decision is',
563
557
  const setItem = storage.setItem
564
558
  storage.setItem = () => { throw new Error('blocked') }
565
559
  try {
566
- expect(writeConsentLanguage('pl', receiving)).toBe(false)
560
+ expect(consentLinkHelper.writeConsentLanguage('pl', receiving)).toBe(false)
567
561
  } finally {
568
562
  // The mock storage is shared by every test in the file.
569
563
  storage.setItem = setItem
@@ -607,7 +601,7 @@ describe('language: strictly necessary, stored whatever the cookie decision is',
607
601
  test('a stored decision of any kind lets the link\'s language through, and still wins over its decision', () => {
608
602
  for (const stored of [refused, accepted, withRetiredKey]) {
609
603
  env.reset()
610
- writeConsent(stored)
604
+ consentStorageHelper.writeConsent(stored)
611
605
  arrive('pl', { [CONSENT_ANALYTICS]: !stored[CONSENT_ANALYTICS], [CONSENT_MARKETING]: true })
612
606
 
613
607
  const store = makeConsentStore()
@@ -669,9 +663,9 @@ describe('language: strictly necessary, stored whatever the cookie decision is',
669
663
  })
670
664
 
671
665
  describe('the inline fragment', () => {
672
- const run = (opts: Parameters<typeof consentLinkerScript>[0] = receiving) => {
666
+ const run = (opts: Parameters<typeof consentLinkHelper.consentLinkerScript>[0] = receiving) => {
673
667
  // eslint-disable-next-line no-new-func
674
- new Function(consentLinkerScript(opts))()
668
+ new Function(consentLinkHelper.consentLinkerScript(opts))()
675
669
  }
676
670
  const all = { linker: { domains: DOMAINS, language: { supported: ['en', 'pl', 'de', 'fr'] } } }
677
671
 
@@ -680,7 +674,7 @@ describe('language: strictly necessary, stored whatever the cookie decision is',
680
674
  run()
681
675
 
682
676
  expect(env.store.get(CONSENT_LANGUAGE_KEY)).toBe('pl')
683
- expect(readConsent()).toBeNull()
677
+ expect(consentStorageHelper.readConsent()).toBeNull()
684
678
  expect(env.href()).toBe('https://platform.test/dispatcher')
685
679
  })
686
680
 
@@ -689,26 +683,26 @@ describe('language: strictly necessary, stored whatever the cookie decision is',
689
683
  run(all)
690
684
 
691
685
  expect(env.store.get(CONSENT_LANGUAGE_KEY)).toBe('fr')
692
- expect(readConsent()?.[CONSENT_ANALYTICS]).toBe(true)
686
+ expect(consentStorageHelper.readConsent()?.[CONSENT_ANALYTICS]).toBe(true)
693
687
  })
694
688
 
695
689
  test('a link that carries "reject all" adopts the decision and stores the language', () => {
696
690
  arrive('fr', refused)
697
691
  run(all)
698
692
 
699
- expect(readConsent()?.[CONSENT_ANALYTICS]).toBe(false)
693
+ expect(consentStorageHelper.readConsent()?.[CONSENT_ANALYTICS]).toBe(false)
700
694
  expect(env.store.get(CONSENT_LANGUAGE_KEY)).toBe('fr')
701
695
  })
702
696
 
703
697
  test('a stored record of any kind lets the language through, and the link\'s decision never overwrites it', () => {
704
698
  for (const stored of [refused, accepted, withRetiredKey]) {
705
699
  env.reset()
706
- writeConsent(stored)
700
+ consentStorageHelper.writeConsent(stored)
707
701
  arrive('de', { [CONSENT_ANALYTICS]: !stored[CONSENT_ANALYTICS], [CONSENT_MARKETING]: true })
708
702
  run()
709
703
 
710
704
  expect(env.store.get(CONSENT_LANGUAGE_KEY)).toBe('de')
711
- expect(readConsent()?.[CONSENT_ANALYTICS]).toBe(stored[CONSENT_ANALYTICS])
705
+ expect(consentStorageHelper.readConsent()?.[CONSENT_ANALYTICS]).toBe(stored[CONSENT_ANALYTICS])
712
706
  }
713
707
  })
714
708
 
@@ -755,14 +749,14 @@ describe('language: strictly necessary, stored whatever the cookie decision is',
755
749
  })
756
750
 
757
751
  test('a page that only sends carries no language code at all', () => {
758
- const script = consentLinkerScript({ linker: { domains: DOMAINS, language: {} } })
752
+ const script = consentLinkHelper.consentLinkerScript({ linker: { domains: DOMAINS, language: {} } })
759
753
 
760
754
  expect(script).not.toContain(CONSENT_LANGUAGE_KEY)
761
- expect(consentLinkerScript(linkerOpts)).toBe(script)
755
+ expect(consentLinkHelper.consentLinkerScript(linkerOpts)).toBe(script)
762
756
  })
763
757
 
764
758
  test('the bootstrap embeds it, after the defaults and before its own storage read', () => {
765
- const script = consentBootstrapScript(receiving)
759
+ const script = consentModeHelper.consentBootstrapScript(receiving)
766
760
 
767
761
  expect(script.indexOf(CONSENT_LANGUAGE_KEY)).toBeGreaterThan(script.indexOf("'consent','default'"))
768
762
  // The fragment reads storage itself (to know whether a decision exists) before it writes the
@@ -771,7 +765,7 @@ describe('language: strictly necessary, stored whatever the cookie decision is',
771
765
  })
772
766
 
773
767
  test('escapes "<" in a configured language too', () => {
774
- const script = consentLinkerScript({ linker: { domains: DOMAINS, language: { supported: ['<b>'], storageKey: '<k>' } } })
768
+ const script = consentLinkHelper.consentLinkerScript({ linker: { domains: DOMAINS, language: { supported: ['<b>'], storageKey: '<k>' } } })
775
769
 
776
770
  expect(script).not.toContain('<b>')
777
771
  expect(script).not.toContain('<k>')
@@ -1,11 +1,11 @@
1
1
  import { describe, test, expect } from 'bun:test'
2
2
  import { CONSENT_ANALYTICS, CONSENT_ESSENTIAL, CONSENT_EVENT, CONSENT_KEY, CONSENT_MARKETING } from '../src/consts.js'
3
- import { consentGateScript, trackingGranted } from '../src/gtm.js'
3
+ import { consentModeHelper } from '../src/gtobserver.js'
4
4
  import type { ConsentCategory, ConsentRecord } from '../src/types.js'
5
5
 
6
6
  describe('trackingGranted', () => {
7
7
  test('no record at all is not granted', () => {
8
- expect(trackingGranted(null)).toBe(false)
8
+ expect(consentModeHelper.trackingGranted(null)).toBe(false)
9
9
  })
10
10
 
11
11
  test('an essential-only record is not granted — required categories never count', () => {
@@ -13,19 +13,19 @@ describe('trackingGranted', () => {
13
13
  [CONSENT_ESSENTIAL]: true, [CONSENT_ANALYTICS]: false, [CONSENT_MARKETING]: false,
14
14
  }
15
15
 
16
- expect(trackingGranted(record)).toBe(false)
16
+ expect(consentModeHelper.trackingGranted(record)).toBe(false)
17
17
  })
18
18
 
19
19
  test('analytics granted is tracking-granted', () => {
20
20
  const record: ConsentRecord = { [CONSENT_ESSENTIAL]: true, [CONSENT_ANALYTICS]: true }
21
21
 
22
- expect(trackingGranted(record)).toBe(true)
22
+ expect(consentModeHelper.trackingGranted(record)).toBe(true)
23
23
  })
24
24
 
25
25
  test('marketing granted is tracking-granted too', () => {
26
26
  const record: ConsentRecord = { [CONSENT_ESSENTIAL]: true, [CONSENT_MARKETING]: true }
27
27
 
28
- expect(trackingGranted(record)).toBe(true)
28
+ expect(consentModeHelper.trackingGranted(record)).toBe(true)
29
29
  })
30
30
 
31
31
  test('a required category is ignored even when the record marks it granted', () => {
@@ -39,7 +39,7 @@ describe('trackingGranted', () => {
39
39
  },
40
40
  ]
41
41
 
42
- expect(trackingGranted({ essential: true }, categories)).toBe(false)
42
+ expect(consentModeHelper.trackingGranted({ essential: true }, categories)).toBe(false)
43
43
  })
44
44
 
45
45
  test('a category with no signals is ignored even when granted', () => {
@@ -47,7 +47,7 @@ describe('trackingGranted', () => {
47
47
  { key: 'preferences', labelKey: 'a', descriptionKey: 'b' },
48
48
  ]
49
49
 
50
- expect(trackingGranted({ preferences: true }, categories)).toBe(false)
50
+ expect(consentModeHelper.trackingGranted({ preferences: true }, categories)).toBe(false)
51
51
  })
52
52
  })
53
53
 
@@ -91,7 +91,7 @@ describe('consentGateScript — the two reachable paths', () => {
91
91
  test('a returning visitor with a stored grant runs the loader immediately, synchronously', () => {
92
92
  const browser = makeBrowser({ record: { essential: true, analytics: true, v: 2 } })
93
93
  browser.win.__loaded = 0
94
- browser.run(consentGateScript('window.__loaded = (window.__loaded || 0) + 1'))
94
+ browser.run(consentModeHelper.consentGateScript('window.__loaded = (window.__loaded || 0) + 1'))
95
95
 
96
96
  expect(browser.win.__loaded).toBe(1)
97
97
  // Nothing left listening — the immediate branch never subscribes.
@@ -101,7 +101,7 @@ describe('consentGateScript — the two reachable paths', () => {
101
101
  test('a stored record that grants nothing does not run the loader immediately', () => {
102
102
  const browser = makeBrowser({ record: { essential: true, analytics: false, v: 2 } })
103
103
  browser.win.__loaded = 0
104
- browser.run(consentGateScript('window.__loaded = (window.__loaded || 0) + 1'))
104
+ browser.run(consentModeHelper.consentGateScript('window.__loaded = (window.__loaded || 0) + 1'))
105
105
 
106
106
  expect(browser.win.__loaded).toBe(0)
107
107
  expect(browser.listenerCount()).toBe(1)
@@ -110,7 +110,7 @@ describe('consentGateScript — the two reachable paths', () => {
110
110
  test('no stored record at all falls to the event-listener branch and waits', () => {
111
111
  const browser = makeBrowser()
112
112
  browser.win.__loaded = 0
113
- browser.run(consentGateScript('window.__loaded = (window.__loaded || 0) + 1'))
113
+ browser.run(consentModeHelper.consentGateScript('window.__loaded = (window.__loaded || 0) + 1'))
114
114
 
115
115
  expect(browser.win.__loaded).toBe(0)
116
116
 
@@ -145,7 +145,7 @@ describe('consentGateScript — the two reachable paths', () => {
145
145
  const doc = { cookie: `${CONSENT_KEY}=${record}` }
146
146
  // eslint-disable-next-line no-new-func
147
147
  new Function(
148
- 'window', 'document', consentGateScript('window.__loaded = (window.__loaded || 0) + 1')
148
+ 'window', 'document', consentModeHelper.consentGateScript('window.__loaded = (window.__loaded || 0) + 1')
149
149
  )(win, doc)
150
150
 
151
151
  expect(win.__loaded).toBe(1)
@@ -158,7 +158,7 @@ describe('consentGateScript — the two reachable paths', () => {
158
158
  ]
159
159
  const browser = makeBrowser({ record: { essential: true, analytics: false, v: 2 } })
160
160
  browser.win.__loaded = 0
161
- browser.run(consentGateScript('window.__loaded = (window.__loaded || 0) + 1', { categories }))
161
+ browser.run(consentModeHelper.consentGateScript('window.__loaded = (window.__loaded || 0) + 1', { categories }))
162
162
 
163
163
  expect(browser.win.__loaded).toBe(0)
164
164
 
@@ -167,7 +167,7 @@ describe('consentGateScript — the two reachable paths', () => {
167
167
  })
168
168
 
169
169
  test('the generated script carries both reachable code paths in its text', () => {
170
- const script = consentGateScript('window.__loaded = (window.__loaded || 0) + 1')
170
+ const script = consentModeHelper.consentGateScript('window.__loaded = (window.__loaded || 0) + 1')
171
171
 
172
172
  // The immediate branch: `load()` called right after the stored-record check, before any
173
173
  // listener is attached.
@@ -182,7 +182,7 @@ describe('consentGateScript — the two reachable paths', () => {
182
182
  test('a custom storage key is honoured, matching the bootstrap script\'s own lookup', () => {
183
183
  const browser = makeBrowser({ key: 'custom_key', record: { essential: true, analytics: true, v: 2 } })
184
184
  browser.win.__loaded = 0
185
- browser.run(consentGateScript(
185
+ browser.run(consentModeHelper.consentGateScript(
186
186
  'window.__loaded = (window.__loaded || 0) + 1', { storageKey: 'custom_key' }
187
187
  ))
188
188
 
package/build/gtm.d.ts DELETED
@@ -1,72 +0,0 @@
1
- import type { ConsentCategory, ConsentOptions, ConsentRecord } from './types.js';
2
- /**
3
- * Push onto the tag-manager queue in the shape Google's own snippet uses.
4
- *
5
- * `arguments`, not an array literal. Both work with today's Consent API, but `arguments` is what
6
- * `gtag.js` itself emits, and a page that carries both this and Google's snippet should not have
7
- * two shapes in one queue.
8
- */
9
- export declare function gtagConsent(..._args: unknown[]): void;
10
- /** Every signal the configured categories drive, at its pre-consent value. */
11
- export declare const consentDefaults: (categories?: ConsentCategory[]) => Record<string, 'granted' | 'denied'>;
12
- /** Every signal, at the value this record implies. */
13
- export declare const consentUpdate: (record: ConsentRecord, categories?: ConsentCategory[]) => Record<string, 'granted' | 'denied'>;
14
- /**
15
- * Declare what is denied, before any tag can act.
16
- *
17
- * Idempotent through a window flag, because the page may carry this call twice — once inline in
18
- * the document head, once from whatever bundle mounts the dialog — and a second `default` after a
19
- * tag has loaded is worse than none: it can widen what was already narrowed.
20
- */
21
- export declare const pushConsentDefaults: (opts?: ConsentOptions) => void;
22
- /**
23
- * Apply a decision: globals first, then the signal update, then any per-category events.
24
- *
25
- * The order is load-bearing. A tag that fires on the update event reads the globals in the same
26
- * turn, so writing them afterwards would let the first firing see the previous answer.
27
- */
28
- export declare const applyConsent: (record: ConsentRecord, opts?: ConsentOptions) => void;
29
- /**
30
- * Whether a stored decision grants tracking at all — a signal-bearing category, not merely the
31
- * required essential one.
32
- *
33
- * "Signal-bearing" is what a required category can never be: it exists to gate whether a LOADER
34
- * runs, and a required category is disclosure, not a question — everyone gets it. Only a category
35
- * that both is optional and drives at least one Consent Mode signal counts, which is exactly what
36
- * `googleTagHeadScript`'s `'basic'` mode gates a Google tag behind.
37
- */
38
- export declare const trackingGranted: (record: ConsentRecord | null, categories?: ConsentCategory[]) => boolean;
39
- /**
40
- * The inline script a document stamps ABOVE its tag-manager snippet.
41
- *
42
- * It exists because the defaults have to be on the queue before `gtm.js` loads, and a React bundle
43
- * cannot be: by the time an island mounts, the container has been running for hundreds of
44
- * milliseconds and has already decided what it may do. Emitting it from the HTML is the only
45
- * ordering that holds — and it reads the stored record too, so a returning visitor's tags are not
46
- * denied for the first paint of every page.
47
- */
48
- export declare const consentBootstrapScript: (opts?: ConsentOptions) => string;
49
- /**
50
- * Withhold a loader until a stored or later decision grants tracking.
51
- *
52
- * The counterpart to `consentBootstrapScript` for the `'basic'` gated loading mode
53
- * (`@owlmeans/web-gtm`'s `GOOGLE_TAG_DEFAULT_MODE`): where the bootstrap always declares Consent
54
- * Mode's defaults so a tag may load cookieless, this withholds the tag itself — the loader string
55
- * is only ever reached once `trackingGranted` is true, either because a returning visitor's stored
56
- * record already says so, or because `applyConsent` later dispatches {@link CONSENT_EVENT} with
57
- * one that does. The listener removes itself on the first passing event, so the loader — which
58
- * marks its own element and refuses to run twice — is asked to run at most once from here.
59
- *
60
- * `loaderExpr` is a complete statement or expression that runs the loader when reached — the
61
- * self-invoking IIFE string `googleTagHeadScript`'s `gtagScript`/`gtmContainerScript` already
62
- * produce, embedded here verbatim rather than called. It is placed inline exactly as
63
- * `consentBootstrapScript`'s own output is: dynamic values (the storage key, the category shape,
64
- * the event name) are JSON-encoded so nothing configured can break out of a string literal, and
65
- * escaping the RESULT for HTML (`</script`, `<!--`) is left to whoever composes the final inline
66
- * script — the same discipline `consentBootstrapScript` follows, and `googleTagHeadScript` already
67
- * applies to the whole script it stamps.
68
- */
69
- export declare const consentGateScript: (loaderExpr: string, opts?: ConsentOptions & {
70
- categories?: ConsentCategory[];
71
- }) => string;
72
- //# sourceMappingURL=gtm.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"gtm.d.ts","sourceRoot":"","sources":["../src/gtm.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAUhF;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,GAAG,KAAK,EAAE,OAAO,EAAE,GAAG,IAAI,CAUrD;AAKD,8EAA8E;AAC9E,eAAO,MAAM,eAAe,gBACb,eAAe,EAAE,KAC7B,MAAM,CAAC,MAAM,EAAE,SAAS,GAAG,QAAQ,CAMrC,CAAA;AAED,sDAAsD;AACtD,eAAO,MAAM,aAAa,WAChB,aAAa,eAAe,eAAe,EAAE,KACpD,MAAM,CAAC,MAAM,EAAE,SAAS,GAAG,QAAQ,CAWrC,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,UAAW,cAAc,KAAG,IAO3D,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,YAAY,WAAY,aAAa,SAAS,cAAc,KAAG,IA6B3E,CAAA;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe,WAClB,aAAa,GAAG,IAAI,eAAc,eAAe,EAAE,KAC1D,OAEF,CAAA;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,sBAAsB,UAAW,cAAc,KAAG,MA+B9D,CAAA;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,iBAAiB,eAChB,MAAM,SAAS,cAAc,GAAG;IAAE,UAAU,CAAC,EAAE,eAAe,EAAE,CAAA;CAAE,KAC7E,MAuBF,CAAA"}