@unitninety/site-kit 1.6.4 → 1.6.6

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 (54) hide show
  1. package/README.md +104 -1
  2. package/package.json +4 -3
  3. package/src/bridge/DraftBlocks.tsx +10 -0
  4. package/src/bridge/createBridge.ts +11 -10
  5. package/src/bridge/index.ts +1 -0
  6. package/src/bridge/nativeDraft.ts +22 -4
  7. package/src/bridge/nativeOrder.ts +68 -0
  8. package/src/bridge/registry.ts +3 -2
  9. package/src/bridge/selection.ts +59 -10
  10. package/src/bridge/useDraftContent.tsx +35 -22
  11. package/src/components/BlockLink.tsx +28 -0
  12. package/src/components/Link.tsx +8 -1
  13. package/src/components/block-navigation.ts +24 -0
  14. package/src/custom-code/fragments.ts +64 -0
  15. package/src/custom-code/index.ts +30 -0
  16. package/src/elements/blockAnchor.ts +7 -0
  17. package/src/elements/declaration.ts +1 -1
  18. package/src/elements/define.ts +1 -1
  19. package/src/elements/index.ts +1 -0
  20. package/src/elements/ops/validate.ts +9 -2
  21. package/src/features/client/README.md +1 -2
  22. package/src/features/client/SiteFeatures.tsx +15 -25
  23. package/src/features/client/index.ts +0 -6
  24. package/src/features/client/verification.ts +3 -0
  25. package/src/features/contracts/base.ts +1 -5
  26. package/src/features/contracts/index.ts +0 -20
  27. package/src/features/contracts/verificationConfig.ts +7 -0
  28. package/src/features/server/gateway.ts +1 -3
  29. package/src/features/server/index.ts +0 -2
  30. package/src/legacy/cli.mjs +2 -2
  31. package/src/legacy/content.ts +1 -1
  32. package/src/server/BlockAnchors.tsx +92 -0
  33. package/src/server/CustomCode.tsx +29 -0
  34. package/src/server/index.ts +1 -0
  35. package/src/server/read.ts +13 -0
  36. package/src/server/render.tsx +11 -5
  37. package/src/site/defineSite.ts +6 -3
  38. package/src/site/documents.ts +2 -2
  39. package/src/site/forms.ts +32 -0
  40. package/src/site/index.ts +2 -0
  41. package/src/site/types.ts +4 -1
  42. package/src/site/version.ts +1 -1
  43. package/src/site/wire.ts +3 -0
  44. package/src/features/client/ConsentRuntime.tsx +0 -95
  45. package/src/features/client/useConsent.ts +0 -15
  46. package/src/features/consent/README.md +0 -70
  47. package/src/features/consent/cookieScript.ts +0 -53
  48. package/src/features/consent/custom.ts +0 -89
  49. package/src/features/consent/defaults.ts +0 -40
  50. package/src/features/consent/drivers.ts +0 -233
  51. package/src/features/consent/loader.ts +0 -248
  52. package/src/features/consent/registry.ts +0 -398
  53. package/src/features/contracts/consent.ts +0 -150
  54. package/src/features/server/ConsentDefaults.tsx +0 -15
@@ -1,398 +0,0 @@
1
- import {
2
- classifyManagedIntegration,
3
- ConsentInstallationSchema,
4
- ConsentStateSchema,
5
- deniedConsentState,
6
- type ConsentAdapter,
7
- type ConsentInstallation,
8
- type ConsentState,
9
- } from '../contracts/consent'
10
- import { cookieScriptAdapter } from './cookieScript'
11
- import { customConsentAdapter, type CustomConsentRegistration } from './custom'
12
- import { createBrowserConsentDriver, CONSENT_LOAD_TIMEOUT_MS } from './drivers'
13
- import {
14
- consentLoaderStatus,
15
- createConsentLoader,
16
- observeConsentLoader,
17
- requireConsentReload,
18
- } from './loader'
19
-
20
- type Snapshot = {
21
- state: ConsentState
22
- ready: boolean
23
- error: string | null
24
- reloadRequired: boolean
25
- }
26
- type Identity = {
27
- endpoint: string
28
- provider: string
29
- source: string
30
- adapter?: ConsentAdapter
31
- owner?: CustomConsentRegistration
32
- reopen?: () => void
33
- }
34
- const registries = new WeakMap<Document, ReturnType<typeof createRegistry>>()
35
- export function consentRegistry(window: Window) {
36
- let registry = registries.get(window.document)
37
- if (!registry) {
38
- registry = createRegistry(window)
39
- registries.set(window.document, registry)
40
- }
41
- return registry
42
- }
43
- function identity(
44
- endpoint: string,
45
- installation: ConsentInstallation,
46
- registration?: CustomConsentRegistration,
47
- ): Identity {
48
- return {
49
- endpoint,
50
- provider: installation.provider,
51
- source: installation.scriptURL ?? '',
52
- ...(installation.provider === 'custom'
53
- ? registration?.adapter
54
- ? { adapter: registration.adapter }
55
- : { owner: registration, reopen: registration?.openPreferences }
56
- : {}),
57
- }
58
- }
59
- function same(a: Identity, b: Identity) {
60
- return (
61
- a.endpoint === b.endpoint &&
62
- a.provider === b.provider &&
63
- a.source === b.source &&
64
- a.adapter === b.adapter &&
65
- a.owner === b.owner &&
66
- a.reopen === b.reopen
67
- )
68
- }
69
- function validRegistration(registration?: CustomConsentRegistration) {
70
- if (!registration) return false
71
- if (registration.adapter !== undefined)
72
- return (
73
- registration.openPreferences === undefined &&
74
- !!registration.adapter &&
75
- ['read', 'subscribe', 'openPreferences'].every(
76
- (key) => typeof registration.adapter?.[key as keyof ConsentAdapter] === 'function',
77
- )
78
- )
79
- return typeof registration.openPreferences === 'function'
80
- }
81
- function createRegistry(window: Window) {
82
- const driver = createBrowserConsentDriver(window)
83
- const loader = createConsentLoader(driver)
84
- const listeners = new Set<() => void>()
85
- const owners = new Set<object>()
86
- let endpoint: string | undefined
87
- let enabled = false
88
- let installation: ConsentInstallation | undefined
89
- let registration: CustomConsentRegistration | undefined
90
- let installed: Identity | undefined
91
- let adapter: ConsentAdapter | undefined
92
- let unsubscribe: (() => void) | undefined
93
- let generation = 0
94
- let activated = false
95
- let attempted = false
96
- let script: HTMLScriptElement | undefined
97
- let providerFailed = false
98
- let providerError: string | null = null
99
- let timer: number | undefined
100
- let loadTimer: number | undefined
101
- let raw = deniedConsentState()
102
- let snapshot: Snapshot = { state: raw, ready: false, error: null, reloadRequired: false }
103
- const clearTimer = () => {
104
- if (timer !== undefined) window.clearTimeout(timer)
105
- timer = undefined
106
- }
107
- // Embedding a banner does not make it a consent authority for U90-managed scripts.
108
- const embedOnly = () => installation?.provider === 'custom' && registration === undefined
109
- const registrationComplete = () =>
110
- installation?.provider !== 'custom' || validRegistration(registration)
111
- const effective = () =>
112
- enabled && owners.size === 1 && !!unsubscribe && !providerFailed && registrationComplete()
113
- ? raw
114
- : deniedConsentState()
115
- const publish = () => {
116
- const state = effective()
117
- const status = consentLoaderStatus(driver)
118
- const next = {
119
- state: { ...state },
120
- ready: state.ready,
121
- error: status.reloadRequired
122
- ? status.error
123
- : !registrationComplete() && !embedOnly() && enabled
124
- ? 'Custom consent registration is incomplete. Supply one adapter or a preferences callback.'
125
- : (providerError ?? status.error),
126
- reloadRequired: status.reloadRequired,
127
- }
128
- if (JSON.stringify(next) !== JSON.stringify(snapshot)) {
129
- snapshot = next
130
- for (const listener of listeners) listener()
131
- }
132
- }
133
- observeConsentLoader(driver, () => {
134
- if (consentLoaderStatus(driver).reloadRequired)
135
- driver.setPermission(deniedConsentState(), installation?.googleMode ?? 'basic')
136
- publish()
137
- })
138
- const reconcile = () => {
139
- const state = effective()
140
- const mode = installation?.googleMode ?? 'basic'
141
- const blocked = consentLoaderStatus(driver).reloadRequired
142
- driver.configure(installation?.integrations ?? [])
143
- driver.setPermission(blocked ? deniedConsentState() : state, mode)
144
- // Advanced mode is a request exception only for classified Google code. Generic rows stay category-gated.
145
- const advanced = !blocked && state.ready && mode === 'advanced'
146
- const integrations = installation?.integrations ?? []
147
- const selected = advanced
148
- ? integrations.filter(
149
- (row) => state[row.category] || classifyManagedIntegration(row).kind === 'ga4',
150
- )
151
- : integrations
152
- void loader.apply(advanced ? { ...state, analytics: true } : state, selected).catch(() => {
153
- publish()
154
- })
155
- publish()
156
- }
157
- const receive = (value: unknown) => {
158
- const parsed = ConsentStateSchema.safeParse(value)
159
- raw = parsed.success ? parsed.data : deniedConsentState()
160
- providerError = raw.ready ? null : 'Consent preferences are unavailable. Retry consent setup.'
161
- if (raw.ready) clearTimer()
162
- reconcile()
163
- }
164
- const readAdapter = () => {
165
- if (!adapter) {
166
- reconcile()
167
- return
168
- }
169
- try {
170
- receive(adapter?.read())
171
- } catch {
172
- receive(undefined)
173
- }
174
- }
175
- const disconnect = () => {
176
- ++generation
177
- try {
178
- unsubscribe?.()
179
- } catch {
180
- /* authority is already invalid */
181
- }
182
- unsubscribe = undefined
183
- clearTimer()
184
- raw = deniedConsentState()
185
- }
186
- const subscribe = () => {
187
- if (!adapter || unsubscribe) return
188
- const current = ++generation
189
- try {
190
- unsubscribe = adapter.subscribe((value) => {
191
- if (current === generation && owners.size === 1 && !providerFailed) receive(value)
192
- })
193
- if (typeof unsubscribe !== 'function') throw Error('Subscription unavailable')
194
- activated = true
195
- readAdapter()
196
- } catch {
197
- providerFailed = true
198
- providerError = 'Consent subscription unavailable. Retry consent setup.'
199
- reconcile()
200
- }
201
- }
202
- const watchReadiness = () => {
203
- clearTimer()
204
- if (!raw.ready)
205
- timer = window.setTimeout(() => {
206
- raw = deniedConsentState()
207
- providerError = 'Consent provider did not become ready. Retry consent setup.'
208
- reconcile()
209
- }, CONSENT_LOAD_TIMEOUT_MS)
210
- }
211
- const install = () => {
212
- if (
213
- !installation ||
214
- !enabled ||
215
- providerFailed ||
216
- (!registrationComplete() && !embedOnly()) ||
217
- owners.size !== 1 ||
218
- consentLoaderStatus(driver).reloadRequired
219
- )
220
- return
221
- if (!adapter && !embedOnly()) {
222
- if (installation.provider === 'cookiescript') {
223
- if ((window as Window & { CookieScript?: unknown }).CookieScript !== undefined) {
224
- providerFailed = true
225
- providerError =
226
- 'CookieScript ownership conflicts with an already installed provider. Correct setup and reload.'
227
- reconcile()
228
- return
229
- }
230
- adapter = cookieScriptAdapter(window)
231
- } else {
232
- if (!validRegistration(registration)) {
233
- providerError =
234
- 'Custom consent registration is incomplete. Supply one adapter or a preferences callback.'
235
- reconcile()
236
- return
237
- }
238
- adapter = customConsentAdapter(window, registration)
239
- }
240
- }
241
- const wasSubscribed = !!unsubscribe
242
- subscribe() // subscription and immediate read precede insertion
243
- if (providerFailed) return
244
- if (!attempted) {
245
- script = window.document.createElement('script')
246
- script.async = true
247
- script.src = installation.scriptURL!
248
- script.dataset.u90ConsentProvider = installation.provider
249
- const ownedScript = script
250
- loadTimer = window.setTimeout(() => {
251
- if (script !== ownedScript) return
252
- providerFailed = true
253
- raw = deniedConsentState()
254
- providerError = 'Consent provider load timed out. Reload after checking setup.'
255
- requireConsentReload(driver, providerError)
256
- reconcile()
257
- }, CONSENT_LOAD_TIMEOUT_MS)
258
- script.onload = () => {
259
- if (script !== ownedScript) return
260
- window.clearTimeout(loadTimer)
261
- loadTimer = undefined
262
- if (!providerFailed && adapter) {
263
- readAdapter()
264
- watchReadiness()
265
- }
266
- }
267
- script.onerror = () => {
268
- if (script !== ownedScript) return
269
- window.clearTimeout(loadTimer)
270
- loadTimer = undefined
271
- providerFailed = true
272
- providerError = 'Consent provider failed to load. Retry consent setup.'
273
- raw = deniedConsentState()
274
- script?.remove()
275
- script = undefined
276
- clearTimer()
277
- reconcile()
278
- }
279
- attempted = true
280
- window.document.head.appendChild(script)
281
- }
282
- if (adapter && !wasSubscribed) watchReadiness()
283
- }
284
- return {
285
- read: () => snapshot,
286
- subscribe(listener: () => void) {
287
- listeners.add(listener)
288
- return () => {
289
- listeners.delete(listener)
290
- }
291
- },
292
- attach(owner: object) {
293
- owners.add(owner)
294
- driver.suspend(true)
295
- },
296
- detach(owner: object) {
297
- owners.delete(owner)
298
- driver.suspend(true)
299
- // StrictMode's same-turn replay retains installation; a lasting owner loss revokes.
300
- queueMicrotask(() => {
301
- if (owners.size === 0) {
302
- disconnect()
303
- reconcile()
304
- }
305
- })
306
- },
307
- commit(
308
- owner: object,
309
- nextEndpoint: string,
310
- nextEnabled: boolean,
311
- input?: ConsentInstallation,
312
- custom?: CustomConsentRegistration,
313
- ) {
314
- if (!owners.has(owner)) return
315
- const parsed = input === undefined ? null : ConsentInstallationSchema.safeParse(input)
316
- const next = parsed?.success ? parsed.data : undefined
317
- if (endpoint !== undefined && endpoint !== nextEndpoint) {
318
- disconnect()
319
- enabled = false
320
- reconcile()
321
- if (activated || attempted || driver.attempted())
322
- requireConsentReload(
323
- driver,
324
- 'Reload to apply consent changes. The site consent authority changed.',
325
- )
326
- else {
327
- adapter = undefined
328
- installed = undefined
329
- }
330
- }
331
- endpoint = nextEndpoint
332
- if (next && installation?.googleMode !== next.googleMode && driver.googleAttempted())
333
- requireConsentReload(
334
- driver,
335
- 'Reload to apply consent changes. Google consent mode changed.',
336
- )
337
- installation = next
338
- registration = custom
339
- enabled = !!nextEnabled && !!next && next.provider !== 'none'
340
- driver.suspend(owners.size !== 1)
341
- if (enabled) {
342
- const nextIdentity = identity(nextEndpoint, next!, custom)
343
- if (installed && !same(installed, nextIdentity)) {
344
- disconnect()
345
- providerFailed = true
346
- if (activated || attempted)
347
- requireConsentReload(
348
- driver,
349
- 'Reload to apply consent changes. The consent provider or registration changed.',
350
- )
351
- else {
352
- adapter = undefined
353
- installed = undefined
354
- providerFailed = false
355
- }
356
- }
357
- if (!installed) installed = nextIdentity
358
- if (!consentLoaderStatus(driver).reloadRequired) install()
359
- }
360
- if (!enabled) {
361
- raw = deniedConsentState()
362
- providerError = parsed && !parsed.success ? 'Consent installation is invalid.' : null
363
- }
364
- driver.suspend(owners.size !== 1)
365
- if (enabled && !providerFailed && unsubscribe) readAdapter()
366
- else reconcile()
367
- },
368
- page(path: string) {
369
- driver.page(path)
370
- },
371
- openPreferences() {
372
- // Existing CMP controls remain necessary UI even after disable or reload latch.
373
- try {
374
- if (!adapter || !ConsentStateSchema.parse(adapter.read()).ready || providerFailed)
375
- throw Error('unavailable')
376
- adapter.openPreferences()
377
- readAdapter()
378
- } catch {
379
- raw = deniedConsentState()
380
- providerError = 'Cookie preferences are unavailable. Retry consent setup.'
381
- reconcile()
382
- }
383
- },
384
- retry() {
385
- if (consentLoaderStatus(driver).reloadRequired) return
386
- if (enabled && (providerFailed || !raw.ready)) {
387
- // A wrapper may have caught subscribe() failure while still returning cleanup.
388
- // Retry that same authority from a fresh subscription, retaining its installation.
389
- disconnect()
390
- }
391
- if (providerFailed && attempted && !script) attempted = false
392
- providerFailed = false
393
- providerError = null
394
- install()
395
- readAdapter()
396
- },
397
- }
398
- }
@@ -1,150 +0,0 @@
1
- import { z } from 'zod'
2
-
3
- export type ConsentState = {
4
- version: 1
5
- necessary: true
6
- preferences: boolean
7
- analytics: boolean
8
- marketing: boolean
9
- ready: boolean
10
- }
11
- export type ConsentAdapter = {
12
- read(): ConsentState
13
- subscribe(listener: (state: ConsentState) => void): () => void
14
- openPreferences(): void
15
- }
16
- export type ConsentCategory = 'preferences' | 'analytics' | 'marketing'
17
- export type ManagedIntegration = {
18
- id: string
19
- src: string
20
- category: ConsentCategory
21
- attributes?: Record<string, string>
22
- revocation: 'stop' | 'reload'
23
- }
24
- export type ConsentInstallation = {
25
- provider: 'none' | 'cookiescript' | 'custom'
26
- scriptURL?: string
27
- integrations: ManagedIntegration[]
28
- googleMode: 'basic' | 'advanced'
29
- }
30
- export type IntegrationDriver = {
31
- start(integration: ManagedIntegration): Promise<void>
32
- stop(id: string): Promise<'stopped' | 'reload-required'>
33
- }
34
-
35
- /** Always allocate a new snapshot so consumers cannot mutate shared denial. */
36
- export function deniedConsentState(): ConsentState {
37
- return { version: 1, necessary: true, preferences: false, analytics: false, marketing: false, ready: false }
38
- }
39
- export const ConsentStateSchema = z.strictObject({
40
- version: z.literal(1), necessary: z.literal(true),
41
- preferences: z.boolean(), analytics: z.boolean(), marketing: z.boolean(), ready: z.boolean(),
42
- }).transform((state): ConsentState => state.ready ? state : deniedConsentState())
43
-
44
- /** URL parsing must not silently discard raw controls, fragments, or backslashes. */
45
- const HTTPSURLSchema = z.string().max(2048).refine((source) => {
46
- if (/[\s\\#]/.test(source)) return false
47
- try {
48
- const url = new URL(source)
49
- return url.protocol === 'https:' && !!url.hostname && !url.username && !url.password && !source.includes('@')
50
- } catch { return false }
51
- }, 'Expected an HTTPS URL without userinfo, fragments, whitespace or backslashes.')
52
- .transform((source) => new URL(source).href)
53
-
54
- export function isCookieScriptURL(source: string): boolean {
55
- const parsed = HTTPSURLSchema.safeParse(source)
56
- return parsed.success && /^https:\/\/(?:cdn|dev-cdn)\.cookie-script\.com\/s\/[a-fA-F0-9]{32}\.js$/.test(parsed.data)
57
- }
58
- const ElementIdSchema = z.string().min(1).max(128).regex(/^[A-Za-z][A-Za-z0-9_:.-]*$/)
59
- // Only standard attributes are shipped. No data-* key is approved for this release.
60
- const AttributesSchema = z.strictObject({
61
- id: ElementIdSchema.optional(),
62
- charset: z.string().max(40).regex(/^[A-Za-z0-9._-]+$/).transform((value) => value.toLowerCase()).optional(),
63
- crossorigin: z.enum(['anonymous', 'use-credentials']).optional(),
64
- integrity: z.string().max(1024).refine((value) => value.split(' ').every((token) =>
65
- /^sha256-[A-Za-z0-9+/]{43}=$/.test(token) ||
66
- /^sha384-[A-Za-z0-9+/]{64}$/.test(token) ||
67
- /^sha512-[A-Za-z0-9+/]{86}==$/.test(token)), 'Expected SHA-256, SHA-384 or SHA-512 SRI digests.').optional(),
68
- })
69
- const IntegrationFields = z.strictObject({
70
- id: z.string().trim().min(1).max(128).regex(/^[A-Za-z0-9][A-Za-z0-9_.:-]*$/),
71
- src: HTTPSURLSchema,
72
- category: z.enum(['preferences', 'analytics', 'marketing']),
73
- attributes: AttributesSchema.optional(),
74
- revocation: z.enum(['stop', 'reload']),
75
- })
76
- export const ManagedIntegrationSchema = IntegrationFields
77
- const InstallationFields = z.strictObject({
78
- provider: z.enum(['none', 'cookiescript', 'custom']),
79
- scriptURL: HTTPSURLSchema.optional(),
80
- integrations: z.array(ManagedIntegrationSchema).max(100).default([]),
81
- googleMode: z.enum(['basic', 'advanced']).default('basic'),
82
- })
83
- function validateInstallation(value: ConsentInstallation, context: z.RefinementCtx) {
84
- if (value.provider !== 'none' && !value.scriptURL) {
85
- context.addIssue({ code: 'custom', path: ['scriptURL'], message: 'An active provider requires an installation URL.' })
86
- } else if (value.provider === 'none' && value.scriptURL) {
87
- context.addIssue({ code: 'custom', path: ['scriptURL'], message: 'No provider cannot install a script.' })
88
- } else if (value.provider === 'cookiescript' && !isCookieScriptURL(value.scriptURL!)) {
89
- context.addIssue({ code: 'custom', path: ['scriptURL'], message: 'Use a direct global CookieScript production or staging installation; geographic wrappers are unsupported.' })
90
- }
91
- const ids = new Set<string>()
92
- value.integrations.forEach((integration, index) => {
93
- if (ids.has(integration.id)) context.addIssue({ code: 'custom', path: ['integrations', index, 'id'], message: 'Integration IDs must be unique within the site.' })
94
- ids.add(integration.id)
95
- })
96
- }
97
- export const ConsentInstallationSchema = InstallationFields.superRefine(validateInstallation)
98
- /** Readers tolerate additive fields but retain only known validated installation data. */
99
- export const PublicConsentInstallationSchema = InstallationFields.loose().extend({
100
- integrations: z.array(IntegrationFields.loose().transform((row) => ManagedIntegrationSchema.parse({
101
- id: row.id, src: row.src, category: row.category, revocation: row.revocation,
102
- ...(row.attributes ? { attributes: row.attributes } : {}),
103
- }))).max(100).default([]),
104
- }).superRefine(validateInstallation).transform((row) => ConsentInstallationSchema.parse({
105
- provider: row.provider, ...(row.scriptURL ? { scriptURL: row.scriptURL } : {}),
106
- integrations: row.integrations, googleMode: row.googleMode,
107
- }))
108
-
109
- export type ManagedIntegrationClassification =
110
- | { kind: 'ga4'; measurementId: string }
111
- | { kind: 'contentsquare'; tagId: string }
112
- | { kind: 'custom' }
113
- /** Shared by permissions and drivers. Caller IDs and known hosts confer no authority. */
114
- export function classifyManagedIntegration(input: unknown): ManagedIntegrationClassification {
115
- const parsed = ManagedIntegrationSchema.safeParse(input)
116
- if (!parsed.success) return { kind: 'custom' }
117
- const row = parsed.data
118
- if (row.category !== 'analytics' ||
119
- (row.attributes?.charset !== undefined && row.attributes.charset !== 'utf-8') ||
120
- (row.attributes?.crossorigin !== undefined && row.attributes.crossorigin !== 'anonymous')) return { kind: 'custom' }
121
- // The 1–32 suffix bound is a U90 supported-input limit, not vendor certification.
122
- const ga = /^https:\/\/www\.googletagmanager\.com\/gtag\/js\?id=(G-[A-Z0-9]{1,32})$/.exec(row.src)
123
- if (ga) return { kind: 'ga4', measurementId: ga[1] }
124
- const cs = /^https:\/\/t\.contentsquare\.net\/uxa\/([a-f0-9]{13})\.js$/.exec(row.src)
125
- if (cs && row.revocation === 'reload') return { kind: 'contentsquare', tagId: cs[1] }
126
- return { kind: 'custom' }
127
- }
128
-
129
- function customConfiguration(installation?: ConsentInstallation): string {
130
- if (!installation) return JSON.stringify({ provider: null, integrations: [] })
131
- const normalized = ConsentInstallationSchema.parse(installation)
132
- return JSON.stringify({
133
- provider: normalized.provider === 'custom' ? normalized.scriptURL : null,
134
- integrations: normalized.integrations.filter((row) => classifyManagedIntegration(row).kind === 'custom')
135
- .sort((a, b) => a.id.localeCompare(b.id)).map((row) => ({
136
- id: row.id, src: row.src, category: row.category, revocation: row.revocation,
137
- attributes: Object.fromEntries(Object.entries(row.attributes ?? {}).sort(([a], [b]) => a.localeCompare(b))),
138
- })),
139
- })
140
- }
141
- export type ConsentConfiguration = { enabled: boolean; installation?: ConsentInstallation }
142
- /** The protected writer and all feature toggles must use this same before/after check. */
143
- export function consentChangeRequiresCodeWrite(before: ConsentConfiguration, after: ConsentConfiguration): boolean {
144
- const oldCustom = customConfiguration(before.installation)
145
- const newCustom = customConfiguration(after.installation)
146
- const wasActive = before.enabled && !!before.installation && before.installation.provider !== 'none'
147
- const willBeActive = after.enabled && !!after.installation && after.installation.provider !== 'none'
148
- const activates = (!before.enabled && after.enabled) || (!wasActive && willBeActive)
149
- return oldCustom !== newCustom || (activates && newCustom !== customConfiguration())
150
- }
@@ -1,15 +0,0 @@
1
- import Script from 'next/script'
2
- import { CONSENT_DEFAULTS_ID, CONSENT_DEFAULTS_SCRIPT } from '../consent/defaults'
3
- /** Mount once in the consuming server root layout, before SiteFeatures. */
4
- export function ConsentDefaults() {
5
- return (
6
- // App Router consumers mount this wrapper in their root layout. This rule
7
- // uses the Pages Router _document filename heuristic for reusable packages.
8
- // eslint-disable-next-line @next/next/no-before-interactive-script-outside-document
9
- <Script
10
- id={CONSENT_DEFAULTS_ID}
11
- strategy="beforeInteractive"
12
- dangerouslySetInnerHTML={{ __html: CONSENT_DEFAULTS_SCRIPT }}
13
- />
14
- )
15
- }