@unitninety/site-kit 1.6.5 → 1.7.0

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 (59) hide show
  1. package/README.md +68 -4
  2. package/package.json +5 -4
  3. package/src/bridge/DraftBlocks.tsx +10 -0
  4. package/src/bridge/U90Bridge.tsx +4 -2
  5. package/src/bridge/caret.ts +7 -8
  6. package/src/bridge/createBridge.ts +30 -10
  7. package/src/bridge/diagnostics.ts +61 -0
  8. package/src/bridge/index.ts +3 -0
  9. package/src/bridge/nativeDraft.ts +47 -36
  10. package/src/bridge/nativeOrder.ts +72 -0
  11. package/src/bridge/nativePath.ts +45 -0
  12. package/src/bridge/registry.ts +3 -2
  13. package/src/bridge/selection.ts +59 -10
  14. package/src/bridge/styles.ts +12 -0
  15. package/src/bridge/useDraftContent.tsx +68 -29
  16. package/src/components/BlockLink.tsx +28 -0
  17. package/src/components/Image.tsx +8 -4
  18. package/src/components/Link.tsx +8 -1
  19. package/src/components/block-navigation.ts +24 -0
  20. package/src/components/draft.ts +19 -0
  21. package/src/components/index.ts +1 -1
  22. package/src/custom-code/fragments.ts +64 -0
  23. package/src/custom-code/index.ts +30 -0
  24. package/src/elements/blockAnchor.ts +7 -0
  25. package/src/elements/index.ts +1 -0
  26. package/src/elements/ops/draftPaths.ts +23 -0
  27. package/src/elements/ops/expand.ts +8 -6
  28. package/src/elements/ops/validate.ts +9 -2
  29. package/src/features/client/README.md +1 -2
  30. package/src/features/client/SiteFeatures.tsx +15 -25
  31. package/src/features/client/index.ts +0 -6
  32. package/src/features/client/verification.ts +3 -0
  33. package/src/features/contracts/base.ts +1 -5
  34. package/src/features/contracts/index.ts +0 -20
  35. package/src/features/contracts/verificationConfig.ts +7 -0
  36. package/src/features/server/gateway.ts +1 -3
  37. package/src/features/server/index.ts +0 -2
  38. package/src/legacy/cli.mjs +2 -2
  39. package/src/legacy/content.ts +1 -1
  40. package/src/server/BlockAnchors.tsx +92 -0
  41. package/src/server/CustomCode.tsx +29 -0
  42. package/src/server/index.ts +1 -0
  43. package/src/server/read.ts +13 -0
  44. package/src/server/render.tsx +12 -6
  45. package/src/site/fields.ts +2 -2
  46. package/src/site/types.ts +4 -2
  47. package/src/site/version.ts +1 -1
  48. package/src/site/wire.ts +3 -0
  49. package/src/features/client/ConsentRuntime.tsx +0 -95
  50. package/src/features/client/useConsent.ts +0 -15
  51. package/src/features/consent/README.md +0 -70
  52. package/src/features/consent/cookieScript.ts +0 -53
  53. package/src/features/consent/custom.ts +0 -89
  54. package/src/features/consent/defaults.ts +0 -40
  55. package/src/features/consent/drivers.ts +0 -233
  56. package/src/features/consent/loader.ts +0 -248
  57. package/src/features/consent/registry.ts +0 -398
  58. package/src/features/contracts/consent.ts +0 -150
  59. package/src/features/server/ConsentDefaults.tsx +0 -15
@@ -1,248 +0,0 @@
1
- import {
2
- ConsentStateSchema,
3
- type ConsentState,
4
- type IntegrationDriver,
5
- type ManagedIntegration,
6
- } from '../contracts/consent'
7
-
8
- export class ConsentReloadRequiredError extends Error {}
9
- const OPERATION_TIMEOUT_MS = 10000
10
- function bounded<T>(operation: Promise<T>, timeout: () => void): Promise<T> {
11
- let timer: ReturnType<typeof setTimeout>
12
- return Promise.race([
13
- operation,
14
- new Promise<never>((_, reject) => {
15
- timer = setTimeout(() => {
16
- timeout()
17
- reject(new ConsentReloadRequiredError('Consent operation timed out; reload required.'))
18
- }, OPERATION_TIMEOUT_MS)
19
- }),
20
- ]).finally(() => clearTimeout(timer))
21
- }
22
-
23
- type RecordState = {
24
- integration: ManagedIntegration
25
- fingerprint: string
26
- desired: boolean
27
- attempted: boolean
28
- active: boolean
29
- revoked: boolean
30
- start?: Promise<void>
31
- stop?: Promise<void>
32
- error?: string
33
- }
34
- type Store = {
35
- records: Map<string, RecordState>
36
- reloadRequired: boolean
37
- error: string | null
38
- listeners: Set<() => void>
39
- }
40
- const stores = new WeakMap<IntegrationDriver, Store>()
41
- function storeFor(driver: IntegrationDriver): Store {
42
- let store = stores.get(driver)
43
- if (!store) {
44
- store = { records: new Map(), reloadRequired: false, error: null, listeners: new Set() }
45
- stores.set(driver, store)
46
- }
47
- return store
48
- }
49
- export function executableFingerprint(integration: ManagedIntegration): string {
50
- return JSON.stringify([
51
- integration.src,
52
- Object.entries(integration.attributes ?? {}).sort(([a], [b]) => a.localeCompare(b)),
53
- ])
54
- }
55
- /** Internal diagnostics; only the fixed loader contract is exposed by the package. */
56
- export function consentLoaderStatus(driver: IntegrationDriver) {
57
- const { reloadRequired, error } = storeFor(driver)
58
- return { reloadRequired, error }
59
- }
60
- export function observeConsentLoader(driver: IntegrationDriver, listener: () => void) {
61
- const listeners = storeFor(driver).listeners
62
- listeners.add(listener)
63
- return () => {
64
- listeners.delete(listener)
65
- }
66
- }
67
- function publish(store: Store) {
68
- for (const listener of store.listeners) listener()
69
- }
70
- export function requireConsentReload(driver: IntegrationDriver, reason: string) {
71
- const store = storeFor(driver)
72
- store.reloadRequired = true
73
- store.error = reason
74
- for (const record of store.records.values()) {
75
- record.desired = false
76
- void stopRecord(driver, store, record).catch(() => {})
77
- }
78
- publish(store)
79
- }
80
- function stopRecord(
81
- driver: IntegrationDriver,
82
- store: Store,
83
- record: RecordState,
84
- finalize = false,
85
- ): Promise<void> {
86
- if (!record.attempted || (record.revoked && !finalize)) return record.stop ?? Promise.resolve()
87
- record.revoked = true
88
- record.active = false
89
- // Invoke before constructing/awaiting any queue: drivers synchronously latch their veto.
90
- let operation: Promise<'stopped' | 'reload-required'>
91
- try {
92
- operation = driver.stop(record.integration.id)
93
- } catch (cause) {
94
- operation = Promise.reject(cause)
95
- }
96
- const stop = bounded(operation, () =>
97
- requireConsentReload(driver, 'Reload to apply consent changes. Integration stop timed out.'),
98
- )
99
- .then(
100
- (result) => {
101
- if (result === 'reload-required' || record.integration.revocation === 'reload')
102
- requireConsentReload(
103
- driver,
104
- 'Reload to apply consent changes. Previously started code requires a fresh document.',
105
- )
106
- },
107
- (cause) => {
108
- requireConsentReload(
109
- driver,
110
- `Reload to apply consent changes. Integration stop failed: ${cause instanceof Error ? cause.message : 'unavailable'}`,
111
- )
112
- throw cause
113
- },
114
- )
115
- .finally(() => {
116
- if (record.stop === stop) record.stop = undefined
117
- publish(store)
118
- })
119
- record.stop = stop
120
- return stop
121
- }
122
- function startRecord(driver: IntegrationDriver, store: Store, record: RecordState): Promise<void> {
123
- if (record.start) return record.start
124
- if (!record.desired || store.reloadRequired || record.active)
125
- return record.stop ?? Promise.resolve()
126
- const begin = async () => {
127
- if (record.stop) await record.stop
128
- if (!record.desired || store.reloadRequired || record.active) return
129
- record.attempted = true
130
- record.revoked = false
131
- record.error = undefined
132
- try {
133
- let expired = false
134
- const operation = driver.start(record.integration)
135
- void operation.then(
136
- () => {
137
- if (expired) void stopRecord(driver, store, record, true).catch(() => {})
138
- },
139
- () => {},
140
- )
141
- await bounded(operation, () => {
142
- expired = true
143
- requireConsentReload(
144
- driver,
145
- 'Reload to apply consent changes. Integration initialization timed out.',
146
- )
147
- })
148
- if (!record.desired || record.revoked || store.reloadRequired) {
149
- await stopRecord(driver, store, record, true)
150
- } else record.active = true
151
- } catch (cause) {
152
- if (cause instanceof ConsentReloadRequiredError) requireConsentReload(driver, cause.message)
153
- record.error = cause instanceof Error ? cause.message : 'Integration initialization failed'
154
- store.error = record.error
155
- publish(store)
156
- throw cause
157
- }
158
- }
159
- // begin invokes start synchronously until its first await.
160
- const starting = begin().finally(() => {
161
- if (record.start === starting) record.start = undefined
162
- publish(store)
163
- })
164
- record.start = starting
165
- return starting
166
- }
167
- export function createConsentLoader(driver: IntegrationDriver): {
168
- apply(state: ConsentState, integrations: ManagedIntegration[]): Promise<void>
169
- dispose(): void
170
- } {
171
- const store = storeFor(driver)
172
- let disposed = false
173
- return {
174
- async apply(input, integrations) {
175
- if (disposed) return
176
- const parsed = ConsentStateSchema.safeParse(input)
177
- const state = parsed.success ? parsed.data : null
178
- const incoming = new Map(integrations.map((row) => [row.id, row]))
179
- const operations: Promise<void>[] = []
180
- // Record *all* restrictions first, independently of asynchronous failures.
181
- for (const [id, record] of store.records) {
182
- const next = incoming.get(id)
183
- if (next && executableFingerprint(next) !== record.fingerprint && record.attempted)
184
- requireConsentReload(
185
- driver,
186
- 'Reload to apply consent changes. An executed integration was replaced.',
187
- )
188
- record.desired = !store.reloadRequired && !!next && !!state?.ready && state[next.category]
189
- if (next && executableFingerprint(next) === record.fingerprint) record.integration = next
190
- if (!record.desired) operations.push(stopRecord(driver, store, record))
191
- }
192
- // A different ID must not slip past a stop that may latch document reload.
193
- const pendingStops = [...store.records.values()].flatMap((record) =>
194
- record.stop ? [record.stop] : [],
195
- )
196
- const stopped = pendingStops.length ? Promise.allSettled(pendingStops) : undefined
197
- for (const integration of integrations) {
198
- let record = store.records.get(integration.id)
199
- if (
200
- !record ||
201
- (!record.attempted && executableFingerprint(integration) !== record.fingerprint)
202
- ) {
203
- record = {
204
- integration,
205
- fingerprint: executableFingerprint(integration),
206
- desired: false,
207
- attempted: false,
208
- active: false,
209
- revoked: false,
210
- }
211
- store.records.set(integration.id, record)
212
- }
213
- record.desired = !store.reloadRequired && !!state?.ready && state[integration.category]
214
- if (record.desired) {
215
- const current = record
216
- operations.push(
217
- (stopped
218
- ? stopped.then(() => {
219
- if (!disposed) return startRecord(driver, store, current)
220
- })
221
- : startRecord(driver, store, current)
222
- ).then(async () => {
223
- // A regrant during an unresolved start/stop resumes only once both settled.
224
- if (!disposed && current.desired && !current.active && !store.reloadRequired)
225
- await startRecord(driver, store, current)
226
- }),
227
- )
228
- }
229
- }
230
- const results = await Promise.allSettled(operations)
231
- const failure = results.find(
232
- (result): result is PromiseRejectedResult => result.status === 'rejected',
233
- )
234
- if (failure) throw failure.reason
235
- if (!store.reloadRequired && ![...store.records.values()].some((record) => record.error))
236
- store.error = null
237
- publish(store)
238
- },
239
- dispose() {
240
- if (disposed) return
241
- disposed = true
242
- for (const record of store.records.values()) {
243
- record.desired = false
244
- void stopRecord(driver, store, record).catch(() => {})
245
- }
246
- },
247
- }
248
- }
@@ -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
- }