@unitninety/site-kit 0.0.0-stage → 1.6.1
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 +12 -2
- package/package.json +79 -4
- package/src/bridge/DraftBlocks.tsx +70 -0
- package/src/bridge/PreviewCommitBoundary.tsx +19 -0
- package/src/bridge/U90Bridge.tsx +54 -0
- package/src/bridge/caret.ts +317 -0
- package/src/bridge/client.tsx +66 -0
- package/src/bridge/createBridge.ts +1010 -0
- package/src/bridge/hooks.tsx +56 -0
- package/src/bridge/index.ts +9 -0
- package/src/bridge/lexical.ts +177 -0
- package/src/bridge/lexicalDom.ts +212 -0
- package/src/bridge/preserveBlockMoves.ts +88 -0
- package/src/bridge/preview.ts +179 -0
- package/src/bridge/protocol.ts +271 -0
- package/src/bridge/registry.ts +27 -0
- package/src/bridge/styles.ts +137 -0
- package/src/components/DateText.tsx +9 -0
- package/src/components/File.tsx +9 -0
- package/src/components/Image.tsx +30 -0
- package/src/components/Link.tsx +24 -0
- package/src/components/NumberText.tsx +9 -0
- package/src/components/RichText.tsx +47 -0
- package/src/components/Text.tsx +15 -0
- package/src/components/Video.tsx +12 -0
- package/src/components/VideoPlayback.tsx +19 -0
- package/src/components/draft.ts +9 -0
- package/src/components/imageProps.ts +26 -0
- package/src/components/index.ts +12 -0
- package/src/components/videoProps.ts +6 -0
- package/src/elements/containers/group.ts +33 -0
- package/src/elements/containers/list.ts +31 -0
- package/src/elements/containers/zone.ts +41 -0
- package/src/elements/declaration.ts +118 -0
- package/src/elements/define.ts +195 -0
- package/src/elements/ids.ts +50 -0
- package/src/elements/index.ts +35 -0
- package/src/elements/jsonSchema.ts +36 -0
- package/src/elements/leaf.ts +98 -0
- package/src/elements/leaves/boolean.ts +30 -0
- package/src/elements/leaves/choice.ts +53 -0
- package/src/elements/leaves/date.ts +39 -0
- package/src/elements/leaves/file.ts +56 -0
- package/src/elements/leaves/image.ts +160 -0
- package/src/elements/leaves/link.ts +149 -0
- package/src/elements/leaves/number.ts +52 -0
- package/src/elements/leaves/query.ts +101 -0
- package/src/elements/leaves/relation.ts +69 -0
- package/src/elements/leaves/rich-text.ts +155 -0
- package/src/elements/leaves/selection.ts +40 -0
- package/src/elements/leaves/single.ts +29 -0
- package/src/elements/leaves/text.ts +103 -0
- package/src/elements/leaves/video.ts +108 -0
- package/src/elements/ops/expand.ts +94 -0
- package/src/elements/ops/overlay.ts +59 -0
- package/src/elements/ops/refs.ts +45 -0
- package/src/elements/ops/seed.ts +185 -0
- package/src/elements/ops/text.ts +20 -0
- package/src/elements/ops/translatable.ts +19 -0
- package/src/elements/ops/upgrade.ts +43 -0
- package/src/elements/ops/validate.ts +56 -0
- package/src/elements/order.ts +10 -0
- package/src/elements/path.ts +43 -0
- package/src/elements/prefix.ts +2 -0
- package/src/elements/registry.ts +48 -0
- package/src/elements/tokens.ts +14 -0
- package/src/elements/walk.ts +42 -0
- package/src/features/client/AskAiProvider.tsx +551 -0
- package/src/features/client/AskAiWidget.tsx +222 -0
- package/src/features/client/ConsentRuntime.tsx +95 -0
- package/src/features/client/FeedbackProvider.tsx +231 -0
- package/src/features/client/FeedbackWidget.tsx +101 -0
- package/src/features/client/PageReporter.tsx +26 -0
- package/src/features/client/README.md +131 -0
- package/src/features/client/SiteFeatures.tsx +250 -0
- package/src/features/client/askAiRecovery.ts +92 -0
- package/src/features/client/askAiStream.ts +59 -0
- package/src/features/client/context.tsx +24 -0
- package/src/features/client/index.ts +24 -0
- package/src/features/client/overlay.tsx +129 -0
- package/src/features/client/styles.css +337 -0
- package/src/features/client/transport.ts +310 -0
- package/src/features/client/useAskAI.ts +45 -0
- package/src/features/client/useConsent.ts +15 -0
- package/src/features/client/useFeedback.ts +33 -0
- package/src/features/client/useSiteSearch.ts +50 -0
- package/src/features/client/verification.ts +141 -0
- package/src/features/consent/README.md +70 -0
- package/src/features/consent/cookieScript.ts +53 -0
- package/src/features/consent/custom.ts +89 -0
- package/src/features/consent/defaults.ts +40 -0
- package/src/features/consent/drivers.ts +233 -0
- package/src/features/consent/loader.ts +248 -0
- package/src/features/consent/registry.ts +398 -0
- package/src/features/contracts/askAi.ts +166 -0
- package/src/features/contracts/askAiSettings.ts +42 -0
- package/src/features/contracts/base.ts +107 -0
- package/src/features/contracts/consent.ts +150 -0
- package/src/features/contracts/feedback.ts +22 -0
- package/src/features/contracts/feedbackSettings.ts +49 -0
- package/src/features/contracts/index.ts +61 -0
- package/src/features/contracts/knowledge.ts +99 -0
- package/src/features/contracts/knowledgePolicy.ts +249 -0
- package/src/features/contracts/search.ts +24 -0
- package/src/features/contracts/verification.ts +52 -0
- package/src/features/contracts/verificationConfig.ts +9 -0
- package/src/features/server/ConsentDefaults.tsx +15 -0
- package/src/features/server/README.md +185 -0
- package/src/features/server/gateway.ts +318 -0
- package/src/features/server/http.ts +179 -0
- package/src/features/server/index.ts +6 -0
- package/src/features/server/session.ts +23 -0
- package/src/features/server/verification.ts +39 -0
- package/src/features/server/verifyVisitor.ts +69 -0
- package/src/languages/config.d.mts +5 -0
- package/src/languages/config.mjs +53 -0
- package/src/languages/decide.ts +72 -0
- package/src/languages/index.ts +4 -0
- package/src/languages/proxy.ts +108 -0
- package/src/languages/routing.ts +93 -0
- package/src/languages/wire.ts +19 -0
- package/src/legacy/bridge.tsx +1987 -0
- package/src/legacy/caret.ts +246 -0
- package/src/legacy/cli.mjs +189 -0
- package/src/legacy/content.ts +993 -0
- package/src/legacy/contract.ts +664 -0
- package/src/legacy/defineBlocks.ts +1602 -0
- package/src/legacy/defineSite.ts +623 -0
- package/src/legacy/defineSite.types.ts +270 -0
- package/src/legacy/draft.tsx +128 -0
- package/src/legacy/image.ts +433 -0
- package/src/legacy/index.ts +209 -0
- package/src/legacy/language-switcher.tsx +58 -0
- package/src/legacy/languageHints.ts +153 -0
- package/src/legacy/languages/fields.ts +131 -0
- package/src/legacy/languages/hash.ts +98 -0
- package/src/legacy/languages/health.ts +97 -0
- package/src/legacy/languages/identity.ts +247 -0
- package/src/legacy/languages/links.ts +81 -0
- package/src/legacy/languages/resolution.ts +56 -0
- package/src/legacy/languages/routing.ts +169 -0
- package/src/legacy/languages/runtime.ts +216 -0
- package/src/legacy/languages/seo.ts +95 -0
- package/src/legacy/languages/settings.ts +211 -0
- package/src/legacy/languages/types.ts +85 -0
- package/src/legacy/languages.ts +8 -0
- package/src/legacy/lexical.ts +170 -0
- package/src/legacy/lexicalDom.ts +166 -0
- package/src/legacy/media.ts +272 -0
- package/src/legacy/nav.ts +176 -0
- package/src/legacy/preview.ts +79 -0
- package/src/legacy/redirects.ts +474 -0
- package/src/legacy/richText.tsx +104 -0
- package/src/legacy/routes.ts +414 -0
- package/src/legacy/seo.ts +327 -0
- package/src/legacy/shape.ts +412 -0
- package/src/legacy/uploads.ts +233 -0
- package/src/legacy/verification.ts +42 -0
- package/src/preview-lock/contracts.ts +42 -0
- package/src/preview-lock/index.ts +3 -0
- package/src/preview-lock/screen.tsx +244 -0
- package/src/preview-lock/server.ts +416 -0
- package/src/redirects/config.d.mts +5 -0
- package/src/redirects/config.mjs +22 -0
- package/src/redirects/index.ts +3 -0
- package/src/redirects/location.ts +31 -0
- package/src/redirects/match.ts +199 -0
- package/src/redirects/proxy.ts +91 -0
- package/src/redirects/wire.ts +30 -0
- package/src/seo/jsonLd.ts +70 -0
- package/src/seo/metadata.ts +127 -0
- package/src/seo/robots.ts +14 -0
- package/src/seo/sitemap.ts +22 -0
- package/src/seo/title.ts +22 -0
- package/src/server/adapters.ts +73 -0
- package/src/server/createU90.ts +96 -0
- package/src/server/handlers.ts +164 -0
- package/src/server/index.ts +6 -0
- package/src/server/read.ts +207 -0
- package/src/server/render.tsx +46 -0
- package/src/server/routes.tsx +171 -0
- package/src/site/assemble.ts +109 -0
- package/src/site/defineSite.ts +132 -0
- package/src/site/documents.ts +18 -0
- package/src/site/fields.ts +59 -0
- package/src/site/index.ts +14 -0
- package/src/site/replies.ts +118 -0
- package/src/site/store.ts +45 -0
- package/src/site/types.ts +75 -0
- package/src/site/version.ts +1 -0
- package/src/site/wire.ts +125 -0
- package/src/switcher/LanguageSwitcher.tsx +42 -0
- package/src/test-preview/README.md +90 -0
- package/src/test-preview/binding.ts +54 -0
- package/src/test-preview/bridge.ts +16 -0
- package/src/test-preview/guard.ts +186 -0
- package/src/test-preview/media.ts +23 -0
- package/src/test-preview/protocol.d.mts +67 -0
- package/src/test-preview/protocol.mjs +222 -0
- package/src/test-preview/runtime.ts +168 -0
- package/src/test-preview/server.ts +10 -0
- package/src/testing/fake.ts +148 -0
- package/src/testing/index.ts +2 -0
- package/src/testing/seed.ts +2 -0
|
@@ -0,0 +1,398 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { PageContextSchema, FeatureErrorCodeSchema, type FeatureErrorCode } from './base'
|
|
3
|
+
import { KnowledgeReferenceSchema, type KnowledgeReference } from './knowledge'
|
|
4
|
+
|
|
5
|
+
export const AskAiInputSchema = z.strictObject({
|
|
6
|
+
requestId: z.uuid(),
|
|
7
|
+
conversationId: z
|
|
8
|
+
.string()
|
|
9
|
+
.min(1)
|
|
10
|
+
.max(128)
|
|
11
|
+
.regex(/^[a-zA-Z0-9_-]+$/)
|
|
12
|
+
.optional(),
|
|
13
|
+
message: z.string().trim().min(1).max(2000),
|
|
14
|
+
page: PageContextSchema,
|
|
15
|
+
})
|
|
16
|
+
export type AskAiInput = z.infer<typeof AskAiInputSchema>
|
|
17
|
+
export type AiProvider = 'anthropic' | 'openai'
|
|
18
|
+
export type AiUsage = {
|
|
19
|
+
/** All billed input, including cache reads and writes exactly once. */
|
|
20
|
+
inputTokens: number
|
|
21
|
+
outputTokens: number
|
|
22
|
+
cacheReadTokens: number | null
|
|
23
|
+
cacheWriteTokens: number | null
|
|
24
|
+
}
|
|
25
|
+
export const AiSourceSchema = KnowledgeReferenceSchema
|
|
26
|
+
export type AiSource = KnowledgeReference
|
|
27
|
+
export type AiTurnState = 'queued' | 'streaming' | 'completed' | 'failed' | 'aborted' | 'unknown'
|
|
28
|
+
type AiEventBody =
|
|
29
|
+
| { type: 'start'; conversationId: string; messageId: string }
|
|
30
|
+
| { type: 'delta'; text: string }
|
|
31
|
+
| { type: 'sources'; sources: AiSource[] }
|
|
32
|
+
| { type: 'usage'; usage: AiUsage }
|
|
33
|
+
| { type: 'done'; state: 'completed' }
|
|
34
|
+
| {
|
|
35
|
+
type: 'error'
|
|
36
|
+
code: FeatureErrorCode
|
|
37
|
+
message: string
|
|
38
|
+
state: 'failed' | 'aborted' | 'unknown'
|
|
39
|
+
}
|
|
40
|
+
export type AiStreamEvent = AiEventBody & { version: 1; requestId: string; sequence: number }
|
|
41
|
+
|
|
42
|
+
/** Shared producer/gateway/client runtime boundary. Sizes measure serialized UTF-8, never Content-Length. */
|
|
43
|
+
const wireBytes = (value: unknown) => new TextEncoder().encode(JSON.stringify(value)).byteLength
|
|
44
|
+
const position = z.int().nonnegative().max(Number.MAX_SAFE_INTEGER)
|
|
45
|
+
const opaqueId = z
|
|
46
|
+
.string()
|
|
47
|
+
.min(1)
|
|
48
|
+
.max(128)
|
|
49
|
+
.regex(/^[A-Za-z0-9_-]+$/)
|
|
50
|
+
const textScalars = z
|
|
51
|
+
.string()
|
|
52
|
+
.refine(
|
|
53
|
+
(v) =>
|
|
54
|
+
!/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/u.test(v) &&
|
|
55
|
+
!v.includes('\u0000'),
|
|
56
|
+
)
|
|
57
|
+
export const AiUsageSchema = z
|
|
58
|
+
.strictObject({
|
|
59
|
+
inputTokens: position,
|
|
60
|
+
outputTokens: position,
|
|
61
|
+
cacheReadTokens: position.nullable(),
|
|
62
|
+
cacheWriteTokens: position.nullable(),
|
|
63
|
+
})
|
|
64
|
+
.refine((v) => (v.cacheReadTokens ?? 0) + (v.cacheWriteTokens ?? 0) <= v.inputTokens)
|
|
65
|
+
export const AiSourcesSchema = z
|
|
66
|
+
.array(AiSourceSchema)
|
|
67
|
+
.max(5)
|
|
68
|
+
.refine((v) => v.every((s) => wireBytes(s) <= 4096) && wireBytes(v) <= 20480)
|
|
69
|
+
const envelope = { version: z.literal(1), requestId: z.uuid(), sequence: position }
|
|
70
|
+
export const AiStreamEventSchema = z.discriminatedUnion('type', [
|
|
71
|
+
z.strictObject({
|
|
72
|
+
...envelope,
|
|
73
|
+
type: z.literal('start'),
|
|
74
|
+
conversationId: opaqueId,
|
|
75
|
+
messageId: opaqueId,
|
|
76
|
+
}),
|
|
77
|
+
z.strictObject({ ...envelope, type: z.literal('delta'), text: textScalars }),
|
|
78
|
+
z.strictObject({ ...envelope, type: z.literal('sources'), sources: AiSourcesSchema }),
|
|
79
|
+
z.strictObject({ ...envelope, type: z.literal('usage'), usage: AiUsageSchema }),
|
|
80
|
+
z.strictObject({ ...envelope, type: z.literal('done'), state: z.literal('completed') }),
|
|
81
|
+
z.strictObject({
|
|
82
|
+
...envelope,
|
|
83
|
+
type: z.literal('error'),
|
|
84
|
+
code: FeatureErrorCodeSchema,
|
|
85
|
+
message: z.string().max(2000),
|
|
86
|
+
state: z.enum(['failed', 'aborted', 'unknown']),
|
|
87
|
+
}),
|
|
88
|
+
])
|
|
89
|
+
export const AI_RECOVERY_MAX_BYTES = 131072
|
|
90
|
+
export const AiMessagePartSchema = z
|
|
91
|
+
.strictObject({
|
|
92
|
+
messageId: opaqueId,
|
|
93
|
+
requestId: z.uuid(),
|
|
94
|
+
sequence: position,
|
|
95
|
+
role: z.enum(['user', 'assistant']),
|
|
96
|
+
state: z.enum(['queued', 'streaming', 'completed', 'failed', 'aborted', 'unknown']),
|
|
97
|
+
contentRevision: position,
|
|
98
|
+
text: textScalars.refine((v) => wireBytes(v) <= 32768),
|
|
99
|
+
textStart: position,
|
|
100
|
+
textEnd: position,
|
|
101
|
+
totalLength: position,
|
|
102
|
+
lastPart: z.boolean(),
|
|
103
|
+
sources: AiSourcesSchema,
|
|
104
|
+
})
|
|
105
|
+
.refine(
|
|
106
|
+
(v) =>
|
|
107
|
+
v.textEnd === v.textStart + v.text.length &&
|
|
108
|
+
v.textEnd <= v.totalLength &&
|
|
109
|
+
v.lastPart === (v.textEnd === v.totalLength) &&
|
|
110
|
+
wireBytes(v) <= 57344,
|
|
111
|
+
)
|
|
112
|
+
export const AiConversationPageSchema = z
|
|
113
|
+
.strictObject({
|
|
114
|
+
version: z.literal(1),
|
|
115
|
+
conversationId: opaqueId,
|
|
116
|
+
historyEpoch: position,
|
|
117
|
+
snapshotUpperSequence: position,
|
|
118
|
+
parts: z.array(AiMessagePartSchema).max(8),
|
|
119
|
+
coverage: z.strictObject({
|
|
120
|
+
newestSequence: position.nullable(),
|
|
121
|
+
oldestSequence: position.nullable(),
|
|
122
|
+
completeMessages: z.array(opaqueId).max(8),
|
|
123
|
+
}),
|
|
124
|
+
nextCursor: z
|
|
125
|
+
.string()
|
|
126
|
+
.min(1)
|
|
127
|
+
.max(1024)
|
|
128
|
+
.regex(/^[A-Za-z0-9_-]+$/)
|
|
129
|
+
.nullable(),
|
|
130
|
+
hasEarlier: z.boolean(),
|
|
131
|
+
})
|
|
132
|
+
.refine(
|
|
133
|
+
(v) =>
|
|
134
|
+
wireBytes(v) <= AI_RECOVERY_MAX_BYTES &&
|
|
135
|
+
v.parts.every((p) => p.sequence <= v.snapshotUpperSequence) &&
|
|
136
|
+
v.hasEarlier === (v.nextCursor !== null) &&
|
|
137
|
+
v.coverage.newestSequence === (v.parts[0]?.sequence ?? null) &&
|
|
138
|
+
v.coverage.oldestSequence === (v.parts.at(-1)?.sequence ?? null) &&
|
|
139
|
+
new Set(v.coverage.completeMessages).size === v.coverage.completeMessages.length &&
|
|
140
|
+
v.coverage.completeMessages.every((id) => {
|
|
141
|
+
const parts = v.parts.filter((p) => p.messageId === id)
|
|
142
|
+
return parts.length > 0 && parts[0].textStart === 0 && parts.at(-1)!.lastPart
|
|
143
|
+
}) &&
|
|
144
|
+
v.parts.every((part, i) => {
|
|
145
|
+
const previous = v.parts[i - 1]
|
|
146
|
+
if (!previous) return true
|
|
147
|
+
if (previous.sequence < part.sequence) return false
|
|
148
|
+
if (previous.sequence !== part.sequence) return previous.lastPart
|
|
149
|
+
return (
|
|
150
|
+
previous.messageId === part.messageId &&
|
|
151
|
+
previous.requestId === part.requestId &&
|
|
152
|
+
previous.role === part.role &&
|
|
153
|
+
previous.contentRevision === part.contentRevision &&
|
|
154
|
+
previous.state === part.state &&
|
|
155
|
+
previous.totalLength === part.totalLength &&
|
|
156
|
+
previous.textEnd === part.textStart &&
|
|
157
|
+
JSON.stringify(previous.sources) === JSON.stringify(part.sources)
|
|
158
|
+
)
|
|
159
|
+
}),
|
|
160
|
+
)
|
|
161
|
+
export type AiConversationPage = z.infer<typeof AiConversationPageSchema>
|
|
162
|
+
export type AiMessagePart = z.infer<typeof AiMessagePartSchema>
|
|
163
|
+
export const AiRatingInputSchema = z.strictObject({
|
|
164
|
+
requestId: z.uuid(),
|
|
165
|
+
rating: z.enum(['helpful', 'unhelpful']),
|
|
166
|
+
})
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
const fields = {
|
|
3
|
+
launcherLabel: z.string().trim().min(1).max(80).default('Ask AI'),
|
|
4
|
+
title: z.string().trim().min(1).max(120).default('Ask AI'),
|
|
5
|
+
greeting: z.string().trim().min(1).max(500).default('How can we help?'),
|
|
6
|
+
suggestions: z.array(z.string().trim().min(1).max(160)).max(4).default([]),
|
|
7
|
+
errorMessage: z
|
|
8
|
+
.string()
|
|
9
|
+
.trim()
|
|
10
|
+
.min(1)
|
|
11
|
+
.max(200)
|
|
12
|
+
.default('We could not complete that answer. Please try again.'),
|
|
13
|
+
accentColor: z
|
|
14
|
+
.string()
|
|
15
|
+
.regex(/^#[0-9a-fA-F]{6}$/)
|
|
16
|
+
.default('#1746a2'),
|
|
17
|
+
cornerStyle: z.enum(['rounded', 'square']).default('rounded'),
|
|
18
|
+
}
|
|
19
|
+
export const AskAiWidgetSettingsSchema = z.strictObject(fields)
|
|
20
|
+
export type AskAiWidgetSettings = z.infer<typeof AskAiWidgetSettingsSchema>
|
|
21
|
+
export const AskAiWidgetSettingsPatchSchema = z.strictObject({
|
|
22
|
+
launcherLabel: fields.launcherLabel.removeDefault().optional(),
|
|
23
|
+
title: fields.title.removeDefault().optional(),
|
|
24
|
+
greeting: fields.greeting.removeDefault().optional(),
|
|
25
|
+
suggestions: fields.suggestions.removeDefault().optional(),
|
|
26
|
+
errorMessage: fields.errorMessage.removeDefault().optional(),
|
|
27
|
+
accentColor: fields.accentColor.removeDefault().optional(),
|
|
28
|
+
cornerStyle: fields.cornerStyle.removeDefault().optional(),
|
|
29
|
+
})
|
|
30
|
+
export function publicAskAiWidgetSettings(input: unknown): AskAiWidgetSettings {
|
|
31
|
+
const value = input && typeof input === 'object' ? (input as Record<string, unknown>) : {}
|
|
32
|
+
return AskAiWidgetSettingsSchema.parse(
|
|
33
|
+
Object.fromEntries(
|
|
34
|
+
Object.keys(fields)
|
|
35
|
+
.filter((key) => value[key] !== undefined)
|
|
36
|
+
.map((key) => [key, value[key]]),
|
|
37
|
+
),
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
export const PublicAskAiWidgetSettingsSchema = z
|
|
41
|
+
.looseObject(fields)
|
|
42
|
+
.transform(publicAskAiWidgetSettings)
|