@unitninety/site-kit 1.6.5 → 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.
- package/README.md +42 -7
- package/package.json +5 -4
- package/src/bridge/DraftBlocks.tsx +10 -0
- package/src/bridge/createBridge.ts +11 -10
- package/src/bridge/index.ts +1 -0
- package/src/bridge/nativeDraft.ts +22 -4
- package/src/bridge/nativeOrder.ts +68 -0
- package/src/bridge/registry.ts +3 -2
- package/src/bridge/selection.ts +59 -10
- package/src/bridge/useDraftContent.tsx +35 -22
- package/src/components/BlockLink.tsx +28 -0
- package/src/components/Link.tsx +8 -1
- package/src/components/block-navigation.ts +24 -0
- package/src/custom-code/fragments.ts +64 -0
- package/src/custom-code/index.ts +30 -0
- package/src/elements/blockAnchor.ts +7 -0
- package/src/elements/index.ts +1 -0
- package/src/elements/ops/expand.ts +1 -4
- package/src/elements/ops/validate.ts +9 -2
- package/src/features/client/README.md +1 -2
- package/src/features/client/SiteFeatures.tsx +15 -25
- package/src/features/client/index.ts +0 -6
- package/src/features/client/verification.ts +3 -0
- package/src/features/contracts/base.ts +1 -5
- package/src/features/contracts/index.ts +0 -20
- package/src/features/contracts/verificationConfig.ts +7 -0
- package/src/features/server/gateway.ts +1 -3
- package/src/features/server/index.ts +0 -2
- package/src/legacy/cli.mjs +2 -2
- package/src/legacy/content.ts +1 -1
- package/src/server/BlockAnchors.tsx +92 -0
- package/src/server/CustomCode.tsx +29 -0
- package/src/server/index.ts +1 -0
- package/src/server/read.ts +13 -0
- package/src/server/render.tsx +11 -5
- package/src/site/version.ts +1 -1
- package/src/site/wire.ts +3 -0
- package/src/features/client/ConsentRuntime.tsx +0 -95
- package/src/features/client/useConsent.ts +0 -15
- package/src/features/consent/README.md +0 -70
- package/src/features/consent/cookieScript.ts +0 -53
- package/src/features/consent/custom.ts +0 -89
- package/src/features/consent/defaults.ts +0 -40
- package/src/features/consent/drivers.ts +0 -233
- package/src/features/consent/loader.ts +0 -248
- package/src/features/consent/registry.ts +0 -398
- package/src/features/contracts/consent.ts +0 -150
- 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
|
-
}
|