11import type { ConsentState , GcmConsentApi , UseScriptContext } from '../types'
2- import { safeParse , strictObject } from 'valibot'
32import { logger } from '../logger'
4- import { gcmConsentState } from './schemas'
53
64export type { GcmConsentApi }
75
8- // Strict variant rebuilt from the lenient schema's entries — same shape, but
9- // unknown keys produce issues so we can warn on typos without breaking the
10- // lenient `defaultConsent` schema parse used at build time.
11- const gcmConsentStateStrict = strictObject ( gcmConsentState . entries )
6+ // GCMv2 consent categories. Every entry takes `granted` or `denied`.
7+ const CONSENT_CATEGORIES = [
8+ 'ad_storage' ,
9+ 'ad_user_data' ,
10+ 'ad_personalization' ,
11+ 'analytics_storage' ,
12+ 'functionality_storage' ,
13+ 'personalization_storage' ,
14+ 'security_storage' ,
15+ ] as const satisfies readonly ( keyof ConsentState ) [ ]
16+
17+ const CONSENT_CATEGORY_KEYS : ReadonlySet < string > = new Set ( CONSENT_CATEGORIES )
18+
19+ // Fails to compile if `ConsentState` grows a key this validator does not know about.
20+ type AssertNever < T extends never > = T
21+ type _ConsentKeysChecked = AssertNever < Exclude < keyof ConsentState , typeof CONSENT_CATEGORIES [ number ] | 'wait_for_update' | 'region' > >
22+
23+ /**
24+ * Describe what is wrong with one consent entry, or return `null` when it is valid.
25+ * The canonical schema stays lenient so unknown keys pass a schema parse; here we
26+ * reject them, because an unknown key is almost always a typo the user wants to see.
27+ */
28+ function describeConsentIssue ( key : string , value : unknown ) : string | null {
29+ if ( CONSENT_CATEGORY_KEYS . has ( key ) ) {
30+ return value === 'granted' || value === 'denied'
31+ ? null
32+ : `"${ key } " must be "granted" or "denied", received ${ JSON . stringify ( value ) } `
33+ }
34+ if ( key === 'wait_for_update' ) {
35+ return typeof value === 'number' && Number . isFinite ( value )
36+ ? null
37+ : `"wait_for_update" must be a number, received ${ JSON . stringify ( value ) } `
38+ }
39+ if ( key === 'region' ) {
40+ return Array . isArray ( value ) && value . every ( entry => typeof entry === 'string' )
41+ ? null
42+ : `"region" must be an array of strings, received ${ JSON . stringify ( value ) } `
43+ }
44+ return `unknown key "${ key } "`
45+ }
1246
1347/**
1448 * GCMv2 consent contract returned by registry scripts (GA, GTM, future Google Ads, …).
@@ -21,11 +55,15 @@ export interface GcmConsentContract {
2155
2256/** Validate a partial GCMv2 consent state. Logs each issue via the registry-scoped logger. */
2357export function validateConsentState ( log : typeof logger , state : ConsentState , source : string ) {
24- const result = safeParse ( gcmConsentStateStrict , state )
25- if ( result . success )
58+ if ( state === null || typeof state !== 'object' ) {
59+ log . warn ( ` ${ source } : consent state must be an object, received ${ JSON . stringify ( state ) } ` )
2660return
27- for ( const issue of result . issues )
28- log . warn ( `${ source } : ${ issue . message } (path: ${ issue . path ?. map ( p => p . key ) . join ( '.' ) || '<root>' } )` )
61+ }
62+ for ( const key in state ) {
63+ const issue = describeConsentIssue ( key , ( state as Record < string , unknown > ) [ key ] )
64+ if ( issue )
65+ log . warn ( `${ source } : ${ issue } ` )
66+ }
2967}
3068
3169export function attachGcmConsent (
0 commit comments