@unitninety/site-kit 0.0.0-stage → 1.6.2

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 (206) hide show
  1. package/README.md +30 -2
  2. package/package.json +79 -4
  3. package/src/bridge/DraftBlocks.tsx +70 -0
  4. package/src/bridge/DraftEntries.tsx +41 -0
  5. package/src/bridge/PreviewCommitBoundary.tsx +19 -0
  6. package/src/bridge/U90Bridge.tsx +54 -0
  7. package/src/bridge/caret.ts +317 -0
  8. package/src/bridge/client.tsx +66 -0
  9. package/src/bridge/createBridge.ts +1015 -0
  10. package/src/bridge/hooks.tsx +56 -0
  11. package/src/bridge/index.ts +9 -0
  12. package/src/bridge/lexical.ts +177 -0
  13. package/src/bridge/lexicalDom.ts +212 -0
  14. package/src/bridge/preserveBlockMoves.ts +88 -0
  15. package/src/bridge/preview.ts +179 -0
  16. package/src/bridge/protocol.ts +273 -0
  17. package/src/bridge/registry.ts +30 -0
  18. package/src/bridge/styles.ts +137 -0
  19. package/src/components/DateText.tsx +9 -0
  20. package/src/components/Entries.tsx +16 -0
  21. package/src/components/File.tsx +9 -0
  22. package/src/components/Image.tsx +30 -0
  23. package/src/components/Link.tsx +24 -0
  24. package/src/components/NumberText.tsx +9 -0
  25. package/src/components/RichText.tsx +47 -0
  26. package/src/components/Text.tsx +15 -0
  27. package/src/components/Video.tsx +12 -0
  28. package/src/components/VideoPlayback.tsx +19 -0
  29. package/src/components/draft.ts +9 -0
  30. package/src/components/imageProps.ts +26 -0
  31. package/src/components/index.ts +13 -0
  32. package/src/components/videoProps.ts +6 -0
  33. package/src/elements/containers/group.ts +33 -0
  34. package/src/elements/containers/list.ts +31 -0
  35. package/src/elements/containers/zone.ts +41 -0
  36. package/src/elements/declaration.ts +118 -0
  37. package/src/elements/define.ts +195 -0
  38. package/src/elements/ids.ts +50 -0
  39. package/src/elements/index.ts +35 -0
  40. package/src/elements/jsonSchema.ts +36 -0
  41. package/src/elements/leaf.ts +98 -0
  42. package/src/elements/leaves/boolean.ts +30 -0
  43. package/src/elements/leaves/choice.ts +53 -0
  44. package/src/elements/leaves/date.ts +39 -0
  45. package/src/elements/leaves/file.ts +56 -0
  46. package/src/elements/leaves/image.ts +160 -0
  47. package/src/elements/leaves/link.ts +149 -0
  48. package/src/elements/leaves/number.ts +52 -0
  49. package/src/elements/leaves/query.ts +101 -0
  50. package/src/elements/leaves/relation.ts +69 -0
  51. package/src/elements/leaves/rich-text.ts +155 -0
  52. package/src/elements/leaves/selection.ts +40 -0
  53. package/src/elements/leaves/single.ts +29 -0
  54. package/src/elements/leaves/text.ts +103 -0
  55. package/src/elements/leaves/video.ts +108 -0
  56. package/src/elements/ops/expand.ts +94 -0
  57. package/src/elements/ops/overlay.ts +59 -0
  58. package/src/elements/ops/refs.ts +45 -0
  59. package/src/elements/ops/seed.ts +185 -0
  60. package/src/elements/ops/text.ts +20 -0
  61. package/src/elements/ops/translatable.ts +19 -0
  62. package/src/elements/ops/upgrade.ts +43 -0
  63. package/src/elements/ops/validate.ts +56 -0
  64. package/src/elements/order.ts +10 -0
  65. package/src/elements/path.ts +43 -0
  66. package/src/elements/prefix.ts +2 -0
  67. package/src/elements/registry.ts +48 -0
  68. package/src/elements/tokens.ts +14 -0
  69. package/src/elements/walk.ts +42 -0
  70. package/src/features/client/AskAiProvider.tsx +551 -0
  71. package/src/features/client/AskAiWidget.tsx +222 -0
  72. package/src/features/client/ConsentRuntime.tsx +95 -0
  73. package/src/features/client/FeedbackProvider.tsx +231 -0
  74. package/src/features/client/FeedbackWidget.tsx +101 -0
  75. package/src/features/client/PageReporter.tsx +26 -0
  76. package/src/features/client/README.md +131 -0
  77. package/src/features/client/SiteFeatures.tsx +250 -0
  78. package/src/features/client/askAiRecovery.ts +92 -0
  79. package/src/features/client/askAiStream.ts +59 -0
  80. package/src/features/client/context.tsx +24 -0
  81. package/src/features/client/index.ts +24 -0
  82. package/src/features/client/overlay.tsx +129 -0
  83. package/src/features/client/styles.css +337 -0
  84. package/src/features/client/transport.ts +310 -0
  85. package/src/features/client/useAskAI.ts +45 -0
  86. package/src/features/client/useConsent.ts +15 -0
  87. package/src/features/client/useFeedback.ts +33 -0
  88. package/src/features/client/useSiteSearch.ts +50 -0
  89. package/src/features/client/verification.ts +141 -0
  90. package/src/features/consent/README.md +70 -0
  91. package/src/features/consent/cookieScript.ts +53 -0
  92. package/src/features/consent/custom.ts +89 -0
  93. package/src/features/consent/defaults.ts +40 -0
  94. package/src/features/consent/drivers.ts +233 -0
  95. package/src/features/consent/loader.ts +248 -0
  96. package/src/features/consent/registry.ts +398 -0
  97. package/src/features/contracts/askAi.ts +166 -0
  98. package/src/features/contracts/askAiSettings.ts +42 -0
  99. package/src/features/contracts/base.ts +107 -0
  100. package/src/features/contracts/consent.ts +150 -0
  101. package/src/features/contracts/feedback.ts +22 -0
  102. package/src/features/contracts/feedbackSettings.ts +49 -0
  103. package/src/features/contracts/index.ts +61 -0
  104. package/src/features/contracts/knowledge.ts +99 -0
  105. package/src/features/contracts/knowledgePolicy.ts +249 -0
  106. package/src/features/contracts/search.ts +24 -0
  107. package/src/features/contracts/verification.ts +52 -0
  108. package/src/features/contracts/verificationConfig.ts +9 -0
  109. package/src/features/server/ConsentDefaults.tsx +15 -0
  110. package/src/features/server/README.md +185 -0
  111. package/src/features/server/gateway.ts +318 -0
  112. package/src/features/server/http.ts +179 -0
  113. package/src/features/server/index.ts +6 -0
  114. package/src/features/server/session.ts +23 -0
  115. package/src/features/server/verification.ts +39 -0
  116. package/src/features/server/verifyVisitor.ts +69 -0
  117. package/src/languages/config.d.mts +5 -0
  118. package/src/languages/config.mjs +53 -0
  119. package/src/languages/decide.ts +72 -0
  120. package/src/languages/index.ts +4 -0
  121. package/src/languages/proxy.ts +108 -0
  122. package/src/languages/routing.ts +93 -0
  123. package/src/languages/wire.ts +19 -0
  124. package/src/legacy/bridge.tsx +1987 -0
  125. package/src/legacy/caret.ts +246 -0
  126. package/src/legacy/cli.mjs +189 -0
  127. package/src/legacy/content.ts +993 -0
  128. package/src/legacy/contract.ts +664 -0
  129. package/src/legacy/defineBlocks.ts +1602 -0
  130. package/src/legacy/defineSite.ts +623 -0
  131. package/src/legacy/defineSite.types.ts +270 -0
  132. package/src/legacy/draft.tsx +128 -0
  133. package/src/legacy/image.ts +433 -0
  134. package/src/legacy/index.ts +209 -0
  135. package/src/legacy/language-switcher.tsx +58 -0
  136. package/src/legacy/languageHints.ts +153 -0
  137. package/src/legacy/languages/fields.ts +131 -0
  138. package/src/legacy/languages/hash.ts +98 -0
  139. package/src/legacy/languages/health.ts +97 -0
  140. package/src/legacy/languages/identity.ts +247 -0
  141. package/src/legacy/languages/links.ts +81 -0
  142. package/src/legacy/languages/resolution.ts +56 -0
  143. package/src/legacy/languages/routing.ts +169 -0
  144. package/src/legacy/languages/runtime.ts +216 -0
  145. package/src/legacy/languages/seo.ts +95 -0
  146. package/src/legacy/languages/settings.ts +211 -0
  147. package/src/legacy/languages/types.ts +85 -0
  148. package/src/legacy/languages.ts +8 -0
  149. package/src/legacy/lexical.ts +170 -0
  150. package/src/legacy/lexicalDom.ts +166 -0
  151. package/src/legacy/media.ts +272 -0
  152. package/src/legacy/nav.ts +176 -0
  153. package/src/legacy/preview.ts +79 -0
  154. package/src/legacy/redirects.ts +474 -0
  155. package/src/legacy/richText.tsx +104 -0
  156. package/src/legacy/routes.ts +414 -0
  157. package/src/legacy/seo.ts +327 -0
  158. package/src/legacy/shape.ts +412 -0
  159. package/src/legacy/uploads.ts +233 -0
  160. package/src/legacy/verification.ts +42 -0
  161. package/src/preview-lock/contracts.ts +42 -0
  162. package/src/preview-lock/index.ts +3 -0
  163. package/src/preview-lock/screen.tsx +244 -0
  164. package/src/preview-lock/server.ts +416 -0
  165. package/src/redirects/config.d.mts +5 -0
  166. package/src/redirects/config.mjs +22 -0
  167. package/src/redirects/index.ts +3 -0
  168. package/src/redirects/location.ts +31 -0
  169. package/src/redirects/match.ts +199 -0
  170. package/src/redirects/proxy.ts +91 -0
  171. package/src/redirects/wire.ts +30 -0
  172. package/src/seo/jsonLd.ts +70 -0
  173. package/src/seo/metadata.ts +127 -0
  174. package/src/seo/robots.ts +14 -0
  175. package/src/seo/sitemap.ts +22 -0
  176. package/src/seo/title.ts +22 -0
  177. package/src/server/adapters.ts +73 -0
  178. package/src/server/createU90.ts +96 -0
  179. package/src/server/handlers.ts +164 -0
  180. package/src/server/index.ts +6 -0
  181. package/src/server/read.ts +207 -0
  182. package/src/server/render.tsx +46 -0
  183. package/src/server/routes.tsx +171 -0
  184. package/src/site/assemble.ts +109 -0
  185. package/src/site/defineSite.ts +132 -0
  186. package/src/site/documents.ts +18 -0
  187. package/src/site/fields.ts +59 -0
  188. package/src/site/index.ts +14 -0
  189. package/src/site/replies.ts +118 -0
  190. package/src/site/store.ts +45 -0
  191. package/src/site/types.ts +75 -0
  192. package/src/site/version.ts +1 -0
  193. package/src/site/wire.ts +125 -0
  194. package/src/switcher/LanguageSwitcher.tsx +42 -0
  195. package/src/test-preview/README.md +90 -0
  196. package/src/test-preview/binding.ts +54 -0
  197. package/src/test-preview/bridge.ts +16 -0
  198. package/src/test-preview/guard.ts +186 -0
  199. package/src/test-preview/media.ts +23 -0
  200. package/src/test-preview/protocol.d.mts +67 -0
  201. package/src/test-preview/protocol.mjs +222 -0
  202. package/src/test-preview/runtime.ts +168 -0
  203. package/src/test-preview/server.ts +10 -0
  204. package/src/testing/fake.ts +148 -0
  205. package/src/testing/index.ts +2 -0
  206. package/src/testing/seed.ts +2 -0
@@ -0,0 +1,107 @@
1
+ import { VerificationConfigSchema, type VerificationConfig } from './verificationConfig'
2
+ import { PublicAskAiWidgetSettingsSchema, type AskAiWidgetSettings } from './askAiSettings'
3
+ import { z } from 'zod'
4
+ import { PublicConsentInstallationSchema, type ConsentInstallation } from './consent'
5
+ import { PublicFeedbackWidgetSettingsSchema, type FeedbackWidgetSettings } from './feedbackSettings'
6
+
7
+ /** Visitor-feature wire version; independent of bridge and Site Kit package versions. */
8
+ export const FEATURE_CONTRACT_VERSION = 1
9
+
10
+ export type FeatureKey = 'search' | 'askAi' | 'feedback' | 'consent'
11
+ export type PageContext = { path: string; title?: string }
12
+ export type FeatureFlags = Record<FeatureKey, boolean>
13
+ export type PublicFeatureBase = {
14
+ version: 1
15
+ verification?: VerificationConfig
16
+ revision: number
17
+ enabled: FeatureFlags
18
+ askAi?: AskAiWidgetSettings
19
+ feedback?: FeedbackWidgetSettings
20
+ consent?: ConsentInstallation
21
+ }
22
+ export type FeatureErrorCode =
23
+ | 'invalid_request'
24
+ | 'unauthorized'
25
+ | 'verification_required'
26
+ | 'forbidden'
27
+ | 'not_found'
28
+ | 'conflict'
29
+ | 'too_large'
30
+ | 'rate_limited'
31
+ | 'budget_exceeded'
32
+ | 'unavailable'
33
+ | 'incompatible_version'
34
+ export type FeatureError = {
35
+ version: 1
36
+ error: { code: FeatureErrorCode; message: string; requestId: string; retryAfter?: number }
37
+ }
38
+
39
+ export const FeatureFlagsSchema = z.strictObject({
40
+ search: z.boolean().default(false),
41
+ askAi: z.boolean().default(false),
42
+ feedback: z.boolean().default(false),
43
+ consent: z.boolean().default(false),
44
+ })
45
+
46
+ const PAGE_ORIGIN = 'https://page-context.invalid'
47
+ const isRelativePath = (path: string): boolean =>
48
+ path.startsWith('/') && !path.startsWith('//') && !/[\\\u0000-\u001f\u007f-\u009f]/.test(path)
49
+
50
+ const PagePathSchema = z
51
+ .string()
52
+ .max(2048)
53
+ .refine(
54
+ isRelativePath,
55
+ 'Expected a root-relative path without backslashes or control characters.',
56
+ )
57
+ .transform((path) => new URL(path, PAGE_ORIGIN).pathname)
58
+ .pipe(z.string().max(2048).refine(isRelativePath, 'Expected a root-relative pathname.'))
59
+
60
+ /** Request context is strict: callers cannot smuggle tenant identity or other reserved fields. */
61
+ export const PageContextSchema = z.strictObject({
62
+ path: PagePathSchema,
63
+ title: z.string().max(200).optional(),
64
+ })
65
+
66
+ /** Readers tolerate additive response fields, while validating every known field. */
67
+ export const PublicFeatureBaseSchema = z.looseObject({
68
+ version: z.literal(FEATURE_CONTRACT_VERSION),
69
+ revision: z.int().nonnegative(),
70
+ verification: VerificationConfigSchema.optional(),
71
+ enabled: FeatureFlagsSchema.loose(),
72
+ askAi: PublicAskAiWidgetSettingsSchema.optional(),
73
+ feedback: PublicFeedbackWidgetSettingsSchema.optional(),
74
+ consent: PublicConsentInstallationSchema.optional(),
75
+ readiness: z
76
+ .object({
77
+ search: z.object({
78
+ ready: z.boolean(),
79
+ state: z.enum(['setup', 'ready', 'indexing', 'failed', 'snapshot']),
80
+ }),
81
+ })
82
+ .optional(),
83
+ })
84
+
85
+ export const FeatureErrorCodeSchema = z.enum([
86
+ 'invalid_request',
87
+ 'unauthorized',
88
+ 'verification_required',
89
+ 'forbidden',
90
+ 'not_found',
91
+ 'conflict',
92
+ 'too_large',
93
+ 'rate_limited',
94
+ 'budget_exceeded',
95
+ 'unavailable',
96
+ 'incompatible_version',
97
+ ])
98
+
99
+ export const FeatureErrorSchema = z.looseObject({
100
+ version: z.literal(FEATURE_CONTRACT_VERSION),
101
+ error: z.looseObject({
102
+ code: FeatureErrorCodeSchema,
103
+ message: z.string(),
104
+ requestId: z.string(),
105
+ retryAfter: z.number().nonnegative().optional(),
106
+ }),
107
+ })
@@ -0,0 +1,150 @@
1
+ import { z } from 'zod'
2
+
3
+ export type ConsentState = {
4
+ version: 1
5
+ necessary: true
6
+ preferences: boolean
7
+ analytics: boolean
8
+ marketing: boolean
9
+ ready: boolean
10
+ }
11
+ export type ConsentAdapter = {
12
+ read(): ConsentState
13
+ subscribe(listener: (state: ConsentState) => void): () => void
14
+ openPreferences(): void
15
+ }
16
+ export type ConsentCategory = 'preferences' | 'analytics' | 'marketing'
17
+ export type ManagedIntegration = {
18
+ id: string
19
+ src: string
20
+ category: ConsentCategory
21
+ attributes?: Record<string, string>
22
+ revocation: 'stop' | 'reload'
23
+ }
24
+ export type ConsentInstallation = {
25
+ provider: 'none' | 'cookiescript' | 'custom'
26
+ scriptURL?: string
27
+ integrations: ManagedIntegration[]
28
+ googleMode: 'basic' | 'advanced'
29
+ }
30
+ export type IntegrationDriver = {
31
+ start(integration: ManagedIntegration): Promise<void>
32
+ stop(id: string): Promise<'stopped' | 'reload-required'>
33
+ }
34
+
35
+ /** Always allocate a new snapshot so consumers cannot mutate shared denial. */
36
+ export function deniedConsentState(): ConsentState {
37
+ return { version: 1, necessary: true, preferences: false, analytics: false, marketing: false, ready: false }
38
+ }
39
+ export const ConsentStateSchema = z.strictObject({
40
+ version: z.literal(1), necessary: z.literal(true),
41
+ preferences: z.boolean(), analytics: z.boolean(), marketing: z.boolean(), ready: z.boolean(),
42
+ }).transform((state): ConsentState => state.ready ? state : deniedConsentState())
43
+
44
+ /** URL parsing must not silently discard raw controls, fragments, or backslashes. */
45
+ const HTTPSURLSchema = z.string().max(2048).refine((source) => {
46
+ if (/[\s\\#]/.test(source)) return false
47
+ try {
48
+ const url = new URL(source)
49
+ return url.protocol === 'https:' && !!url.hostname && !url.username && !url.password && !source.includes('@')
50
+ } catch { return false }
51
+ }, 'Expected an HTTPS URL without userinfo, fragments, whitespace or backslashes.')
52
+ .transform((source) => new URL(source).href)
53
+
54
+ export function isCookieScriptURL(source: string): boolean {
55
+ const parsed = HTTPSURLSchema.safeParse(source)
56
+ return parsed.success && /^https:\/\/(?:cdn|dev-cdn)\.cookie-script\.com\/s\/[a-fA-F0-9]{32}\.js$/.test(parsed.data)
57
+ }
58
+ const ElementIdSchema = z.string().min(1).max(128).regex(/^[A-Za-z][A-Za-z0-9_:.-]*$/)
59
+ // Only standard attributes are shipped. No data-* key is approved for this release.
60
+ const AttributesSchema = z.strictObject({
61
+ id: ElementIdSchema.optional(),
62
+ charset: z.string().max(40).regex(/^[A-Za-z0-9._-]+$/).transform((value) => value.toLowerCase()).optional(),
63
+ crossorigin: z.enum(['anonymous', 'use-credentials']).optional(),
64
+ integrity: z.string().max(1024).refine((value) => value.split(' ').every((token) =>
65
+ /^sha256-[A-Za-z0-9+/]{43}=$/.test(token) ||
66
+ /^sha384-[A-Za-z0-9+/]{64}$/.test(token) ||
67
+ /^sha512-[A-Za-z0-9+/]{86}==$/.test(token)), 'Expected SHA-256, SHA-384 or SHA-512 SRI digests.').optional(),
68
+ })
69
+ const IntegrationFields = z.strictObject({
70
+ id: z.string().trim().min(1).max(128).regex(/^[A-Za-z0-9][A-Za-z0-9_.:-]*$/),
71
+ src: HTTPSURLSchema,
72
+ category: z.enum(['preferences', 'analytics', 'marketing']),
73
+ attributes: AttributesSchema.optional(),
74
+ revocation: z.enum(['stop', 'reload']),
75
+ })
76
+ export const ManagedIntegrationSchema = IntegrationFields
77
+ const InstallationFields = z.strictObject({
78
+ provider: z.enum(['none', 'cookiescript', 'custom']),
79
+ scriptURL: HTTPSURLSchema.optional(),
80
+ integrations: z.array(ManagedIntegrationSchema).max(100).default([]),
81
+ googleMode: z.enum(['basic', 'advanced']).default('basic'),
82
+ })
83
+ function validateInstallation(value: ConsentInstallation, context: z.RefinementCtx) {
84
+ if (value.provider !== 'none' && !value.scriptURL) {
85
+ context.addIssue({ code: 'custom', path: ['scriptURL'], message: 'An active provider requires an installation URL.' })
86
+ } else if (value.provider === 'none' && value.scriptURL) {
87
+ context.addIssue({ code: 'custom', path: ['scriptURL'], message: 'No provider cannot install a script.' })
88
+ } else if (value.provider === 'cookiescript' && !isCookieScriptURL(value.scriptURL!)) {
89
+ context.addIssue({ code: 'custom', path: ['scriptURL'], message: 'Use a direct global CookieScript production or staging installation; geographic wrappers are unsupported.' })
90
+ }
91
+ const ids = new Set<string>()
92
+ value.integrations.forEach((integration, index) => {
93
+ if (ids.has(integration.id)) context.addIssue({ code: 'custom', path: ['integrations', index, 'id'], message: 'Integration IDs must be unique within the site.' })
94
+ ids.add(integration.id)
95
+ })
96
+ }
97
+ export const ConsentInstallationSchema = InstallationFields.superRefine(validateInstallation)
98
+ /** Readers tolerate additive fields but retain only known validated installation data. */
99
+ export const PublicConsentInstallationSchema = InstallationFields.loose().extend({
100
+ integrations: z.array(IntegrationFields.loose().transform((row) => ManagedIntegrationSchema.parse({
101
+ id: row.id, src: row.src, category: row.category, revocation: row.revocation,
102
+ ...(row.attributes ? { attributes: row.attributes } : {}),
103
+ }))).max(100).default([]),
104
+ }).superRefine(validateInstallation).transform((row) => ConsentInstallationSchema.parse({
105
+ provider: row.provider, ...(row.scriptURL ? { scriptURL: row.scriptURL } : {}),
106
+ integrations: row.integrations, googleMode: row.googleMode,
107
+ }))
108
+
109
+ export type ManagedIntegrationClassification =
110
+ | { kind: 'ga4'; measurementId: string }
111
+ | { kind: 'contentsquare'; tagId: string }
112
+ | { kind: 'custom' }
113
+ /** Shared by permissions and drivers. Caller IDs and known hosts confer no authority. */
114
+ export function classifyManagedIntegration(input: unknown): ManagedIntegrationClassification {
115
+ const parsed = ManagedIntegrationSchema.safeParse(input)
116
+ if (!parsed.success) return { kind: 'custom' }
117
+ const row = parsed.data
118
+ if (row.category !== 'analytics' ||
119
+ (row.attributes?.charset !== undefined && row.attributes.charset !== 'utf-8') ||
120
+ (row.attributes?.crossorigin !== undefined && row.attributes.crossorigin !== 'anonymous')) return { kind: 'custom' }
121
+ // The 1–32 suffix bound is a U90 supported-input limit, not vendor certification.
122
+ const ga = /^https:\/\/www\.googletagmanager\.com\/gtag\/js\?id=(G-[A-Z0-9]{1,32})$/.exec(row.src)
123
+ if (ga) return { kind: 'ga4', measurementId: ga[1] }
124
+ const cs = /^https:\/\/t\.contentsquare\.net\/uxa\/([a-f0-9]{13})\.js$/.exec(row.src)
125
+ if (cs && row.revocation === 'reload') return { kind: 'contentsquare', tagId: cs[1] }
126
+ return { kind: 'custom' }
127
+ }
128
+
129
+ function customConfiguration(installation?: ConsentInstallation): string {
130
+ if (!installation) return JSON.stringify({ provider: null, integrations: [] })
131
+ const normalized = ConsentInstallationSchema.parse(installation)
132
+ return JSON.stringify({
133
+ provider: normalized.provider === 'custom' ? normalized.scriptURL : null,
134
+ integrations: normalized.integrations.filter((row) => classifyManagedIntegration(row).kind === 'custom')
135
+ .sort((a, b) => a.id.localeCompare(b.id)).map((row) => ({
136
+ id: row.id, src: row.src, category: row.category, revocation: row.revocation,
137
+ attributes: Object.fromEntries(Object.entries(row.attributes ?? {}).sort(([a], [b]) => a.localeCompare(b))),
138
+ })),
139
+ })
140
+ }
141
+ export type ConsentConfiguration = { enabled: boolean; installation?: ConsentInstallation }
142
+ /** The protected writer and all feature toggles must use this same before/after check. */
143
+ export function consentChangeRequiresCodeWrite(before: ConsentConfiguration, after: ConsentConfiguration): boolean {
144
+ const oldCustom = customConfiguration(before.installation)
145
+ const newCustom = customConfiguration(after.installation)
146
+ const wasActive = before.enabled && !!before.installation && before.installation.provider !== 'none'
147
+ const willBeActive = after.enabled && !!after.installation && after.installation.provider !== 'none'
148
+ const activates = (!before.enabled && after.enabled) || (!wasActive && willBeActive)
149
+ return oldCustom !== newCustom || (activates && newCustom !== customConfiguration())
150
+ }
@@ -0,0 +1,22 @@
1
+ import { z } from 'zod'
2
+ import { PageContextSchema } from './base'
3
+
4
+ export const FEEDBACK_FORM_KEY = 'feedback'
5
+ export const FEEDBACK_FORM_SLUG = 'u90-feedback'
6
+
7
+ /** Presentation may hide or relabel questions; their wire meanings remain fixed. */
8
+ export const FeedbackInputSchema = z
9
+ .strictObject({
10
+ requestId: z.uuid(),
11
+ page: PageContextSchema,
12
+ rating: z
13
+ .union([z.literal(1), z.literal(2), z.literal(3), z.literal(4), z.literal(5)])
14
+ .optional(),
15
+ comment: z.string().trim().min(1).max(2000).optional(),
16
+ })
17
+ .refine((input) => input.rating !== undefined || input.comment !== undefined, {
18
+ message: 'Supply a rating or a useful comment.',
19
+ })
20
+
21
+ export type FeedbackInput = z.output<typeof FeedbackInputSchema>
22
+ export type FeedbackReceipt = { submissionId: string; receivedAt: string }
@@ -0,0 +1,49 @@
1
+ import { z } from 'zod'
2
+ /** Stock presentation only: answer types and the five-point scale stay fixed. */
3
+ const Fields = z.strictObject({
4
+ launcherLabel: z.string().trim().min(1).max(80).default('Feedback'),
5
+ title: z.string().trim().min(1).max(120).default('How was this page?'),
6
+ ratingLabel: z.string().trim().min(1).max(120).default('Rate this page'),
7
+ commentLabel: z.string().trim().min(1).max(120).default('Tell us more'),
8
+ commentPlaceholder: z.string().max(200).default('What worked well, or what could be better?'),
9
+ submitLabel: z.string().trim().min(1).max(80).default('Send feedback'),
10
+ successMessage: z.string().trim().min(1).max(200).default('Thank you for your feedback.'),
11
+ showRating: z.boolean().default(true),
12
+ showComment: z.boolean().default(true),
13
+ })
14
+ export const FeedbackWidgetSettingsSchema = Fields.refine(
15
+ (options) => options.showRating || options.showComment,
16
+ 'Keep at least one question visible.',
17
+ )
18
+ export type FeedbackWidgetSettings = z.infer<typeof FeedbackWidgetSettingsSchema>
19
+
20
+ export const FeedbackWidgetSettingsPatchSchema = z
21
+ .strictObject(
22
+ Object.fromEntries(
23
+ Object.entries(Fields.shape).map(([key, value]) => [key, value.removeDefault().optional()]),
24
+ ) as {
25
+ [K in keyof typeof Fields.shape]: z.ZodOptional<
26
+ ReturnType<(typeof Fields.shape)[K]['removeDefault']>
27
+ >
28
+ },
29
+ )
30
+ .refine(
31
+ (value) =>
32
+ Object.keys(value).length > 0 && Object.values(value).every((entry) => entry !== undefined),
33
+ )
34
+ /** Explicit known keys only; storage or upstream extras never become public display config. */
35
+ export function publicFeedbackWidgetSettings(input: unknown): FeedbackWidgetSettings {
36
+ const row = input && typeof input === 'object' ? (input as Record<string, unknown>) : {}
37
+ return FeedbackWidgetSettingsSchema.parse(
38
+ Object.fromEntries(
39
+ Object.keys(Fields.shape)
40
+ .filter((key) => row[key] !== undefined)
41
+ .map((key) => [key, row[key]]),
42
+ ),
43
+ )
44
+ }
45
+
46
+ /** Additive public responses are tolerated; only the known fields survive parsing. */
47
+ export const PublicFeedbackWidgetSettingsSchema = Fields.loose()
48
+ .refine((options) => options.showRating || options.showComment)
49
+ .transform(publicFeedbackWidgetSettings)
@@ -0,0 +1,61 @@
1
+ export {
2
+ FEATURE_CONTRACT_VERSION,
3
+ FeatureErrorCodeSchema,
4
+ FeatureErrorSchema,
5
+ FeatureFlagsSchema,
6
+ PageContextSchema,
7
+ PublicFeatureBaseSchema,
8
+ } from './base'
9
+ export type {
10
+ FeatureError,
11
+ FeatureErrorCode,
12
+ FeatureFlags,
13
+ FeatureKey,
14
+ PageContext,
15
+ PublicFeatureBase,
16
+ } from './base'
17
+
18
+ export { FeedbackInputSchema, FEEDBACK_FORM_KEY, FEEDBACK_FORM_SLUG } from './feedback'
19
+ export type { FeedbackInput, FeedbackReceipt } from './feedback'
20
+
21
+ export {
22
+ FeedbackWidgetSettingsSchema,
23
+ FeedbackWidgetSettingsPatchSchema,
24
+ publicFeedbackWidgetSettings,
25
+ } from './feedbackSettings'
26
+ export type { FeedbackWidgetSettings } from './feedbackSettings'
27
+
28
+ export {
29
+ ConsentStateSchema,
30
+ ConsentInstallationSchema,
31
+ PublicConsentInstallationSchema,
32
+ ManagedIntegrationSchema,
33
+ deniedConsentState,
34
+ isCookieScriptURL,
35
+ classifyManagedIntegration,
36
+ consentChangeRequiresCodeWrite,
37
+ } from './consent'
38
+ export type {
39
+ ConsentState,
40
+ ConsentAdapter,
41
+ ConsentCategory,
42
+ ConsentInstallation,
43
+ ManagedIntegration,
44
+ IntegrationDriver,
45
+ ManagedIntegrationClassification,
46
+ ConsentConfiguration,
47
+ } from './consent'
48
+
49
+ export * from './knowledge'
50
+ export * from './search'
51
+ export * from './knowledgePolicy'
52
+ export * from './askAi'
53
+
54
+ export {
55
+ AskAiWidgetSettingsSchema,
56
+ AskAiWidgetSettingsPatchSchema,
57
+ publicAskAiWidgetSettings,
58
+ } from './askAiSettings'
59
+ export type { AskAiWidgetSettings } from './askAiSettings'
60
+
61
+ export * from './verification'
@@ -0,0 +1,99 @@
1
+ import { z } from 'zod'
2
+
3
+ export const KNOWLEDGE_LIMITS = {
4
+ documents: 1000,
5
+ bytes: 5 * 1024 * 1024,
6
+ text: 60000,
7
+ title: 200,
8
+ path: 2048,
9
+ } as const
10
+ export const KnowledgeKeySchema = z
11
+ .string()
12
+ .min(1)
13
+ .max(128)
14
+ .regex(/^[a-zA-Z0-9][a-zA-Z0-9:_-]*$/)
15
+ export const KnowledgeHashSchema = z.string().regex(/^[a-f0-9]{64}$/)
16
+ export const KnowledgeTitleSchema = z
17
+ .string()
18
+ .trim()
19
+ .min(1)
20
+ .max(KNOWLEDGE_LIMITS.title)
21
+ .refine((v) => !/[<>\u0000-\u001f\u007f]/.test(v))
22
+ /** Canonical website pathname: no origin, query, fragment, traversal or encoded separators. */
23
+ export const KnowledgePathSchema = z
24
+ .string()
25
+ .max(KNOWLEDGE_LIMITS.path)
26
+ .refine((v) => {
27
+ if (
28
+ !v.startsWith('/') ||
29
+ v.includes('//') ||
30
+ (v.length > 1 && v.endsWith('/')) ||
31
+ /%2f|%5c|%2e/i.test(v) ||
32
+ /[\\?#\u0000-\u0020\u007f]/.test(v)
33
+ )
34
+ return false
35
+ try {
36
+ const decoded = decodeURIComponent(v)
37
+ return (
38
+ !/[\\?#\u0000-\u0020\u007f]/.test(decoded) &&
39
+ !/%[a-f0-9]{2}/i.test(decoded) &&
40
+ !decoded.startsWith('//') &&
41
+ !decoded.split('/').some((s) => s === '.' || s === '..') &&
42
+ new URL(v, 'https://knowledge.invalid').pathname === v
43
+ )
44
+ } catch {
45
+ return false
46
+ }
47
+ })
48
+ const common = {
49
+ id: KnowledgeKeySchema,
50
+ title: KnowledgeTitleSchema,
51
+ fingerprint: KnowledgeHashSchema,
52
+ }
53
+ export const PublishedKnowledgeDocumentSchema = z.strictObject({
54
+ ...common,
55
+ path: KnowledgePathSchema,
56
+ text: z.string().max(KNOWLEDGE_LIMITS.text),
57
+ kind: z.enum(['page', 'entry']),
58
+ })
59
+ export const KnowledgeReferenceSchema = z.union([
60
+ z.strictObject({ ...common, kind: z.enum(['page', 'entry']), path: KnowledgePathSchema }),
61
+ z.strictObject({ ...common, kind: z.literal('faq'), path: KnowledgePathSchema.nullable() }),
62
+ ])
63
+ const hitFields = { excerpt: z.string().max(1000), text: z.string().max(KNOWLEDGE_LIMITS.text) }
64
+ export const KnowledgeHitSchema = z.union([
65
+ KnowledgeReferenceSchema.options[0].extend(hitFields),
66
+ KnowledgeReferenceSchema.options[1].extend(hitFields),
67
+ ])
68
+ export const SourceFlagsSchema = z.strictObject({
69
+ searchable: z.boolean(),
70
+ answerable: z.boolean(),
71
+ })
72
+ export const SnapshotProvenanceSchema = z.strictObject({
73
+ deploymentId: z
74
+ .string()
75
+ .trim()
76
+ .min(1)
77
+ .max(128)
78
+ .regex(/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/),
79
+ sourceHash: KnowledgeHashSchema,
80
+ })
81
+ export const KnowledgeSnapshotSchema = z
82
+ .strictObject({
83
+ sourceKey: KnowledgeKeySchema,
84
+ generation: z.int().positive().max(Number.MAX_SAFE_INTEGER),
85
+ provenance: SnapshotProvenanceSchema,
86
+ documents: z.array(PublishedKnowledgeDocumentSchema).max(KNOWLEDGE_LIMITS.documents),
87
+ })
88
+ .superRefine((v, ctx) => {
89
+ if (new TextEncoder().encode(JSON.stringify(v)).byteLength > KNOWLEDGE_LIMITS.bytes)
90
+ ctx.addIssue({ code: 'custom', message: 'Snapshot exceeds 5 MiB.' })
91
+ for (const key of ['id', 'path'] as const)
92
+ if (new Set(v.documents.map((d) => d[key])).size !== v.documents.length)
93
+ ctx.addIssue({ code: 'custom', message: `Duplicate document ${key}.` })
94
+ })
95
+ export type PublishedKnowledgeDocument = z.infer<typeof PublishedKnowledgeDocumentSchema>
96
+ export type KnowledgeSnapshot = z.infer<typeof KnowledgeSnapshotSchema>
97
+ export type KnowledgeReference = z.infer<typeof KnowledgeReferenceSchema>
98
+ export type KnowledgeHit = KnowledgeReference & { excerpt: string; text: string }
99
+ export type SourceFlags = z.infer<typeof SourceFlagsSchema>