@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.
Files changed (204) hide show
  1. package/README.md +12 -2
  2. package/package.json +79 -4
  3. package/src/bridge/DraftBlocks.tsx +70 -0
  4. package/src/bridge/PreviewCommitBoundary.tsx +19 -0
  5. package/src/bridge/U90Bridge.tsx +54 -0
  6. package/src/bridge/caret.ts +317 -0
  7. package/src/bridge/client.tsx +66 -0
  8. package/src/bridge/createBridge.ts +1010 -0
  9. package/src/bridge/hooks.tsx +56 -0
  10. package/src/bridge/index.ts +9 -0
  11. package/src/bridge/lexical.ts +177 -0
  12. package/src/bridge/lexicalDom.ts +212 -0
  13. package/src/bridge/preserveBlockMoves.ts +88 -0
  14. package/src/bridge/preview.ts +179 -0
  15. package/src/bridge/protocol.ts +271 -0
  16. package/src/bridge/registry.ts +27 -0
  17. package/src/bridge/styles.ts +137 -0
  18. package/src/components/DateText.tsx +9 -0
  19. package/src/components/File.tsx +9 -0
  20. package/src/components/Image.tsx +30 -0
  21. package/src/components/Link.tsx +24 -0
  22. package/src/components/NumberText.tsx +9 -0
  23. package/src/components/RichText.tsx +47 -0
  24. package/src/components/Text.tsx +15 -0
  25. package/src/components/Video.tsx +12 -0
  26. package/src/components/VideoPlayback.tsx +19 -0
  27. package/src/components/draft.ts +9 -0
  28. package/src/components/imageProps.ts +26 -0
  29. package/src/components/index.ts +12 -0
  30. package/src/components/videoProps.ts +6 -0
  31. package/src/elements/containers/group.ts +33 -0
  32. package/src/elements/containers/list.ts +31 -0
  33. package/src/elements/containers/zone.ts +41 -0
  34. package/src/elements/declaration.ts +118 -0
  35. package/src/elements/define.ts +195 -0
  36. package/src/elements/ids.ts +50 -0
  37. package/src/elements/index.ts +35 -0
  38. package/src/elements/jsonSchema.ts +36 -0
  39. package/src/elements/leaf.ts +98 -0
  40. package/src/elements/leaves/boolean.ts +30 -0
  41. package/src/elements/leaves/choice.ts +53 -0
  42. package/src/elements/leaves/date.ts +39 -0
  43. package/src/elements/leaves/file.ts +56 -0
  44. package/src/elements/leaves/image.ts +160 -0
  45. package/src/elements/leaves/link.ts +149 -0
  46. package/src/elements/leaves/number.ts +52 -0
  47. package/src/elements/leaves/query.ts +101 -0
  48. package/src/elements/leaves/relation.ts +69 -0
  49. package/src/elements/leaves/rich-text.ts +155 -0
  50. package/src/elements/leaves/selection.ts +40 -0
  51. package/src/elements/leaves/single.ts +29 -0
  52. package/src/elements/leaves/text.ts +103 -0
  53. package/src/elements/leaves/video.ts +108 -0
  54. package/src/elements/ops/expand.ts +94 -0
  55. package/src/elements/ops/overlay.ts +59 -0
  56. package/src/elements/ops/refs.ts +45 -0
  57. package/src/elements/ops/seed.ts +185 -0
  58. package/src/elements/ops/text.ts +20 -0
  59. package/src/elements/ops/translatable.ts +19 -0
  60. package/src/elements/ops/upgrade.ts +43 -0
  61. package/src/elements/ops/validate.ts +56 -0
  62. package/src/elements/order.ts +10 -0
  63. package/src/elements/path.ts +43 -0
  64. package/src/elements/prefix.ts +2 -0
  65. package/src/elements/registry.ts +48 -0
  66. package/src/elements/tokens.ts +14 -0
  67. package/src/elements/walk.ts +42 -0
  68. package/src/features/client/AskAiProvider.tsx +551 -0
  69. package/src/features/client/AskAiWidget.tsx +222 -0
  70. package/src/features/client/ConsentRuntime.tsx +95 -0
  71. package/src/features/client/FeedbackProvider.tsx +231 -0
  72. package/src/features/client/FeedbackWidget.tsx +101 -0
  73. package/src/features/client/PageReporter.tsx +26 -0
  74. package/src/features/client/README.md +131 -0
  75. package/src/features/client/SiteFeatures.tsx +250 -0
  76. package/src/features/client/askAiRecovery.ts +92 -0
  77. package/src/features/client/askAiStream.ts +59 -0
  78. package/src/features/client/context.tsx +24 -0
  79. package/src/features/client/index.ts +24 -0
  80. package/src/features/client/overlay.tsx +129 -0
  81. package/src/features/client/styles.css +337 -0
  82. package/src/features/client/transport.ts +310 -0
  83. package/src/features/client/useAskAI.ts +45 -0
  84. package/src/features/client/useConsent.ts +15 -0
  85. package/src/features/client/useFeedback.ts +33 -0
  86. package/src/features/client/useSiteSearch.ts +50 -0
  87. package/src/features/client/verification.ts +141 -0
  88. package/src/features/consent/README.md +70 -0
  89. package/src/features/consent/cookieScript.ts +53 -0
  90. package/src/features/consent/custom.ts +89 -0
  91. package/src/features/consent/defaults.ts +40 -0
  92. package/src/features/consent/drivers.ts +233 -0
  93. package/src/features/consent/loader.ts +248 -0
  94. package/src/features/consent/registry.ts +398 -0
  95. package/src/features/contracts/askAi.ts +166 -0
  96. package/src/features/contracts/askAiSettings.ts +42 -0
  97. package/src/features/contracts/base.ts +107 -0
  98. package/src/features/contracts/consent.ts +150 -0
  99. package/src/features/contracts/feedback.ts +22 -0
  100. package/src/features/contracts/feedbackSettings.ts +49 -0
  101. package/src/features/contracts/index.ts +61 -0
  102. package/src/features/contracts/knowledge.ts +99 -0
  103. package/src/features/contracts/knowledgePolicy.ts +249 -0
  104. package/src/features/contracts/search.ts +24 -0
  105. package/src/features/contracts/verification.ts +52 -0
  106. package/src/features/contracts/verificationConfig.ts +9 -0
  107. package/src/features/server/ConsentDefaults.tsx +15 -0
  108. package/src/features/server/README.md +185 -0
  109. package/src/features/server/gateway.ts +318 -0
  110. package/src/features/server/http.ts +179 -0
  111. package/src/features/server/index.ts +6 -0
  112. package/src/features/server/session.ts +23 -0
  113. package/src/features/server/verification.ts +39 -0
  114. package/src/features/server/verifyVisitor.ts +69 -0
  115. package/src/languages/config.d.mts +5 -0
  116. package/src/languages/config.mjs +53 -0
  117. package/src/languages/decide.ts +72 -0
  118. package/src/languages/index.ts +4 -0
  119. package/src/languages/proxy.ts +108 -0
  120. package/src/languages/routing.ts +93 -0
  121. package/src/languages/wire.ts +19 -0
  122. package/src/legacy/bridge.tsx +1987 -0
  123. package/src/legacy/caret.ts +246 -0
  124. package/src/legacy/cli.mjs +189 -0
  125. package/src/legacy/content.ts +993 -0
  126. package/src/legacy/contract.ts +664 -0
  127. package/src/legacy/defineBlocks.ts +1602 -0
  128. package/src/legacy/defineSite.ts +623 -0
  129. package/src/legacy/defineSite.types.ts +270 -0
  130. package/src/legacy/draft.tsx +128 -0
  131. package/src/legacy/image.ts +433 -0
  132. package/src/legacy/index.ts +209 -0
  133. package/src/legacy/language-switcher.tsx +58 -0
  134. package/src/legacy/languageHints.ts +153 -0
  135. package/src/legacy/languages/fields.ts +131 -0
  136. package/src/legacy/languages/hash.ts +98 -0
  137. package/src/legacy/languages/health.ts +97 -0
  138. package/src/legacy/languages/identity.ts +247 -0
  139. package/src/legacy/languages/links.ts +81 -0
  140. package/src/legacy/languages/resolution.ts +56 -0
  141. package/src/legacy/languages/routing.ts +169 -0
  142. package/src/legacy/languages/runtime.ts +216 -0
  143. package/src/legacy/languages/seo.ts +95 -0
  144. package/src/legacy/languages/settings.ts +211 -0
  145. package/src/legacy/languages/types.ts +85 -0
  146. package/src/legacy/languages.ts +8 -0
  147. package/src/legacy/lexical.ts +170 -0
  148. package/src/legacy/lexicalDom.ts +166 -0
  149. package/src/legacy/media.ts +272 -0
  150. package/src/legacy/nav.ts +176 -0
  151. package/src/legacy/preview.ts +79 -0
  152. package/src/legacy/redirects.ts +474 -0
  153. package/src/legacy/richText.tsx +104 -0
  154. package/src/legacy/routes.ts +414 -0
  155. package/src/legacy/seo.ts +327 -0
  156. package/src/legacy/shape.ts +412 -0
  157. package/src/legacy/uploads.ts +233 -0
  158. package/src/legacy/verification.ts +42 -0
  159. package/src/preview-lock/contracts.ts +42 -0
  160. package/src/preview-lock/index.ts +3 -0
  161. package/src/preview-lock/screen.tsx +244 -0
  162. package/src/preview-lock/server.ts +416 -0
  163. package/src/redirects/config.d.mts +5 -0
  164. package/src/redirects/config.mjs +22 -0
  165. package/src/redirects/index.ts +3 -0
  166. package/src/redirects/location.ts +31 -0
  167. package/src/redirects/match.ts +199 -0
  168. package/src/redirects/proxy.ts +91 -0
  169. package/src/redirects/wire.ts +30 -0
  170. package/src/seo/jsonLd.ts +70 -0
  171. package/src/seo/metadata.ts +127 -0
  172. package/src/seo/robots.ts +14 -0
  173. package/src/seo/sitemap.ts +22 -0
  174. package/src/seo/title.ts +22 -0
  175. package/src/server/adapters.ts +73 -0
  176. package/src/server/createU90.ts +96 -0
  177. package/src/server/handlers.ts +164 -0
  178. package/src/server/index.ts +6 -0
  179. package/src/server/read.ts +207 -0
  180. package/src/server/render.tsx +46 -0
  181. package/src/server/routes.tsx +171 -0
  182. package/src/site/assemble.ts +109 -0
  183. package/src/site/defineSite.ts +132 -0
  184. package/src/site/documents.ts +18 -0
  185. package/src/site/fields.ts +59 -0
  186. package/src/site/index.ts +14 -0
  187. package/src/site/replies.ts +118 -0
  188. package/src/site/store.ts +45 -0
  189. package/src/site/types.ts +75 -0
  190. package/src/site/version.ts +1 -0
  191. package/src/site/wire.ts +125 -0
  192. package/src/switcher/LanguageSwitcher.tsx +42 -0
  193. package/src/test-preview/README.md +90 -0
  194. package/src/test-preview/binding.ts +54 -0
  195. package/src/test-preview/bridge.ts +16 -0
  196. package/src/test-preview/guard.ts +186 -0
  197. package/src/test-preview/media.ts +23 -0
  198. package/src/test-preview/protocol.d.mts +67 -0
  199. package/src/test-preview/protocol.mjs +222 -0
  200. package/src/test-preview/runtime.ts +168 -0
  201. package/src/test-preview/server.ts +10 -0
  202. package/src/testing/fake.ts +148 -0
  203. package/src/testing/index.ts +2 -0
  204. 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)