@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.
- package/README.md +30 -2
- package/package.json +79 -4
- package/src/bridge/DraftBlocks.tsx +70 -0
- package/src/bridge/DraftEntries.tsx +41 -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 +1015 -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 +273 -0
- package/src/bridge/registry.ts +30 -0
- package/src/bridge/styles.ts +137 -0
- package/src/components/DateText.tsx +9 -0
- package/src/components/Entries.tsx +16 -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 +13 -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,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>
|