@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.
Files changed (48) hide show
  1. package/README.md +42 -7
  2. package/package.json +5 -4
  3. package/src/bridge/DraftBlocks.tsx +10 -0
  4. package/src/bridge/createBridge.ts +11 -10
  5. package/src/bridge/index.ts +1 -0
  6. package/src/bridge/nativeDraft.ts +22 -4
  7. package/src/bridge/nativeOrder.ts +68 -0
  8. package/src/bridge/registry.ts +3 -2
  9. package/src/bridge/selection.ts +59 -10
  10. package/src/bridge/useDraftContent.tsx +35 -22
  11. package/src/components/BlockLink.tsx +28 -0
  12. package/src/components/Link.tsx +8 -1
  13. package/src/components/block-navigation.ts +24 -0
  14. package/src/custom-code/fragments.ts +64 -0
  15. package/src/custom-code/index.ts +30 -0
  16. package/src/elements/blockAnchor.ts +7 -0
  17. package/src/elements/index.ts +1 -0
  18. package/src/elements/ops/expand.ts +1 -4
  19. package/src/elements/ops/validate.ts +9 -2
  20. package/src/features/client/README.md +1 -2
  21. package/src/features/client/SiteFeatures.tsx +15 -25
  22. package/src/features/client/index.ts +0 -6
  23. package/src/features/client/verification.ts +3 -0
  24. package/src/features/contracts/base.ts +1 -5
  25. package/src/features/contracts/index.ts +0 -20
  26. package/src/features/contracts/verificationConfig.ts +7 -0
  27. package/src/features/server/gateway.ts +1 -3
  28. package/src/features/server/index.ts +0 -2
  29. package/src/legacy/cli.mjs +2 -2
  30. package/src/legacy/content.ts +1 -1
  31. package/src/server/BlockAnchors.tsx +92 -0
  32. package/src/server/CustomCode.tsx +29 -0
  33. package/src/server/index.ts +1 -0
  34. package/src/server/read.ts +13 -0
  35. package/src/server/render.tsx +11 -5
  36. package/src/site/version.ts +1 -1
  37. package/src/site/wire.ts +3 -0
  38. package/src/features/client/ConsentRuntime.tsx +0 -95
  39. package/src/features/client/useConsent.ts +0 -15
  40. package/src/features/consent/README.md +0 -70
  41. package/src/features/consent/cookieScript.ts +0 -53
  42. package/src/features/consent/custom.ts +0 -89
  43. package/src/features/consent/defaults.ts +0 -40
  44. package/src/features/consent/drivers.ts +0 -233
  45. package/src/features/consent/loader.ts +0 -248
  46. package/src/features/consent/registry.ts +0 -398
  47. package/src/features/contracts/consent.ts +0 -150
  48. 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
- }