@unitninety/site-kit 1.6.5 → 1.6.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -7
- package/package.json +5 -4
- package/src/bridge/DraftBlocks.tsx +10 -0
- package/src/bridge/createBridge.ts +11 -10
- package/src/bridge/index.ts +1 -0
- package/src/bridge/nativeDraft.ts +22 -4
- package/src/bridge/nativeOrder.ts +68 -0
- package/src/bridge/registry.ts +3 -2
- package/src/bridge/selection.ts +59 -10
- package/src/bridge/useDraftContent.tsx +35 -22
- package/src/components/BlockLink.tsx +28 -0
- package/src/components/Link.tsx +8 -1
- package/src/components/block-navigation.ts +24 -0
- package/src/custom-code/fragments.ts +64 -0
- package/src/custom-code/index.ts +30 -0
- package/src/elements/blockAnchor.ts +7 -0
- package/src/elements/index.ts +1 -0
- package/src/elements/ops/expand.ts +1 -4
- package/src/elements/ops/validate.ts +9 -2
- package/src/features/client/README.md +1 -2
- package/src/features/client/SiteFeatures.tsx +15 -25
- package/src/features/client/index.ts +0 -6
- package/src/features/client/verification.ts +3 -0
- package/src/features/contracts/base.ts +1 -5
- package/src/features/contracts/index.ts +0 -20
- package/src/features/contracts/verificationConfig.ts +7 -0
- package/src/features/server/gateway.ts +1 -3
- package/src/features/server/index.ts +0 -2
- package/src/legacy/cli.mjs +2 -2
- package/src/legacy/content.ts +1 -1
- package/src/server/BlockAnchors.tsx +92 -0
- package/src/server/CustomCode.tsx +29 -0
- package/src/server/index.ts +1 -0
- package/src/server/read.ts +13 -0
- package/src/server/render.tsx +11 -5
- package/src/site/version.ts +1 -1
- package/src/site/wire.ts +3 -0
- package/src/features/client/ConsentRuntime.tsx +0 -95
- package/src/features/client/useConsent.ts +0 -15
- package/src/features/consent/README.md +0 -70
- package/src/features/consent/cookieScript.ts +0 -53
- package/src/features/consent/custom.ts +0 -89
- package/src/features/consent/defaults.ts +0 -40
- package/src/features/consent/drivers.ts +0 -233
- package/src/features/consent/loader.ts +0 -248
- package/src/features/consent/registry.ts +0 -398
- package/src/features/contracts/consent.ts +0 -150
- package/src/features/server/ConsentDefaults.tsx +0 -15
|
@@ -1,150 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import Script from 'next/script'
|
|
2
|
-
import { CONSENT_DEFAULTS_ID, CONSENT_DEFAULTS_SCRIPT } from '../consent/defaults'
|
|
3
|
-
/** Mount once in the consuming server root layout, before SiteFeatures. */
|
|
4
|
-
export function ConsentDefaults() {
|
|
5
|
-
return (
|
|
6
|
-
// App Router consumers mount this wrapper in their root layout. This rule
|
|
7
|
-
// uses the Pages Router _document filename heuristic for reusable packages.
|
|
8
|
-
// eslint-disable-next-line @next/next/no-before-interactive-script-outside-document
|
|
9
|
-
<Script
|
|
10
|
-
id={CONSENT_DEFAULTS_ID}
|
|
11
|
-
strategy="beforeInteractive"
|
|
12
|
-
dangerouslySetInnerHTML={{ __html: CONSENT_DEFAULTS_SCRIPT }}
|
|
13
|
-
/>
|
|
14
|
-
)
|
|
15
|
-
}
|