@vectoriox/iox-ui 5.2.0 → 5.4.0

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/index.d.ts CHANGED
@@ -1549,6 +1549,66 @@ declare function compileTokenCss(tokens: ReadonlyArray<DesignTokenLike> | null |
1549
1549
  dark: string;
1550
1550
  };
1551
1551
 
1552
+ /**
1553
+ * Flavors (flavors-and-theming-plan.md, F1): the design language a studio launches with,
1554
+ * expressed as data. THE invariant — a flavor never writes a literal into a node; it writes
1555
+ * tokens, and element defaults that REFERENCE tokens. To that end a pack's `defaults` map
1556
+ * holds TOKEN NAMES, not CSS values: the resolver builds `var(--iox-<name>, <packValue>)`,
1557
+ * so smuggling a literal in is impossible by construction, and the validator only has to
1558
+ * check that every referenced token exists.
1559
+ *
1560
+ * The fallback is the bootstrap: until F2 seeds the org's `designtokens` rows, no `--iox-*`
1561
+ * var is defined anywhere, and `var(--iox-radius-control, 4px)` must still paint identically
1562
+ * in the builder canvas and the SSR page. Once tokens ARE seeded (or re-themed), the var wins
1563
+ * over the fallback everywhere — which is exactly the §3 propagation story.
1564
+ */
1565
+ interface FlavorTokenDef {
1566
+ name: string;
1567
+ value: string;
1568
+ /** Dark-scheme override, seeded into DesignToken.values.dark at F2. */
1569
+ values?: {
1570
+ dark?: string;
1571
+ };
1572
+ type?: 'color' | 'spacing' | 'size' | 'font' | 'other';
1573
+ /** Tokens-panel grouping (flavors plan §10 C5). */
1574
+ group?: 'colour' | 'type' | 'space' | 'radius' | 'shadow' | 'motion';
1575
+ }
1576
+ interface FlavorPack {
1577
+ id: string;
1578
+ /** Bumped when the pack's values change; seeding stamps `flavor:<id>@<version>` as DesignToken.source. */
1579
+ version: number;
1580
+ label: string;
1581
+ blurb: string;
1582
+ tokens: FlavorTokenDef[];
1583
+ /** type → styleTrait → TOKEN NAME (never a CSS value — see the module header). */
1584
+ defaults: Record<string, Record<string, string>>;
1585
+ }
1586
+ /** trait → `var(--iox-<token>, <packValue>)` for one node type. Unknown type → {}. */
1587
+ declare function resolveFlavorDefaults(type: string, pack: FlavorPack | null | undefined): Record<string, string>;
1588
+ /**
1589
+ * The ENGINE's half of the cascade: kebab `prop: var(…)` declarations for every pack default
1590
+ * the node's persisted styleProps does NOT set. Appended beside renderDefaultDeclarations —
1591
+ * same guard-on-undefined discipline, so a node saved post-F1 (var already serialised via the
1592
+ * builder's trait defaults) is untouched, and a legacy node picks the pack default up at
1593
+ * render time. Node styleProps always wins.
1594
+ */
1595
+ declare function flavorDefaultDeclarations(type: string, styleProps: Record<string, any> | null | undefined, pack: FlavorPack | null | undefined): string[];
1596
+ /**
1597
+ * The CI gate (flavors plan §10 C4): a pack may only name types and traits that exist in the
1598
+ * live component registry, and tokens it defines itself. Returns human-readable errors; a
1599
+ * consumer spec feeds it the REAL registry and asserts [].
1600
+ */
1601
+ declare function validateFlavorPack(pack: FlavorPack, knownTraitsByType: Record<string, ReadonlySet<string> | string[]>): string[];
1602
+ /**
1603
+ * Swiss — the default pack (flavors plan §8: "the flavor that can never embarrass anyone").
1604
+ * Deliberately SMALL at F1: radius + control padding on the clickables, surface radius on Card.
1605
+ * Colour, type and motion arrive with the brand step (F2) — a colour default here would repaint
1606
+ * legacy pages before anyone chose anything.
1607
+ */
1608
+ declare const SWISS_FLAVOR: FlavorPack;
1609
+ /** What both renderers use until F2 lets an org choose. */
1610
+ declare const DEFAULT_FLAVOR: FlavorPack;
1611
+
1552
1612
  /**
1553
1613
  * page-style — the ONE compiler that turns a page's style settings into CSS.
1554
1614
  *
@@ -2718,5 +2778,5 @@ declare class GalleryLoop {
2718
2778
  private emit;
2719
2779
  }
2720
2780
 
2721
- export { AOSService, BREAKPOINTS, Breakpoint, BreakpointQueryKind, BuilderButtonComponent, BuilderDividerComponent, BuilderHeadingComponent, BuilderIconComponent, BuilderImageComponent, BuilderLinkComponent, BuilderSpacerComponent, ButtonBlockComponent, CONTEXT_ACTION_TYPES, CardComponent, ClientGalleryComponent, ClientListComponent, ClientListItemComponent, ClientRepeaterComponent, ClientSliderContainerComponent, ClientSliderSlideComponent, ComponentInstanceRegistryService, ContainerComponent, ContextStore, DEFAULT_GALLERY_SCHEDULER, DEFAULT_PAGE_TRANSITION, DEFAULT_PAGE_TRANSITION_DURATION, DEFAULT_PAGE_TRANSITION_EASING, DEFAULT_PAGE_TRANSITION_PERSPECTIVE, DEFAULT_PHOSPHOR_WEIGHT, DEFAULT_WIDTH_BY_TYPE, DeclarationContext, FormRenderState, FormRuntimeService, GALLERY_EFFECTS, GALLERY_EFFECT_DESCRIPTORS, GalleryLoop, IOX_BUILDER_EVENTS, IOX_FORM_MESSAGES, IOX_FORM_NAVIGATOR, IOX_FORM_TRANSPORT, IS_PREVIEW, IoxAnimateContainer, IoxAosDirective, IoxAosModule, IoxBuilderComponentsModule, IoxComponentRegistryService, IoxFieldComponent, IoxFormComponent, IoxFormErrorComponent, IoxFormSuccessComponent, IoxPageComponent, IoxPageModule, IoxSubmitButtonComponent, IoxUiModule, LinkedContainerComponent, PAGE_TRANSITION_CATEGORIES, PAGE_TRANSITION_PRESETS, PART_CLASS_NAMES, PIPE_DESCRIPTORS, PIPE_REGISTRY, PageScrollProvider, RESERVED_ALIAS_PREFIX, SUPPRESSED_STYLE_PROPS_BY_TYPE, SWIPER_RESPONSIVE_TRAITS, SectionComponent, TRANSITION_FEELS, TextBlockComponent, VALIDATION_STATES, VALIDATION_STATE_CLASS, VIRTUAL_TRAIT_GROUPS, VIRTUAL_TRAIT_KEYS, ViewPositionDirective, ViewPositionModule, activeActions, applyBindingPipes, applyFeel, breakpointDef, breakpointForWidth, breakpointQueryFor, buildDataSourceFilter, buildPartSelector, buildRelatedFilter, buildSwiperBreakpoints, buildTargetedStateSelector, buildTransitionTimeline, cascadeChain, clearInputOverride, clearOverride, collectContextSlotKeys, collectReferencedAliases, compileDeclarations, compilePageStyle, compileTokenCss, composeVirtualTraits, computeRelative, contextActionEffect, contextSlotKeys, countOverrides, dataSourcePlanToRequest, declaredInputNames, defaultMessageResolver, deriveSeoFromContent, describeSeoFields, effectiveTransitionId, excludeSelf, feelFor, getByPath, getSeoValue, initialSlotValues, isActionEnabled, isContextAction, isInputOverriddenAt, isOverriddenAt, isReservedAlias, isValidSlotKey, legacyEnterToTransition, matchRouteParams, mediaQueryFor, mergeSwiperBreakpoints, nearestCommonAncestorId, nodeCssId, nodeUsesContext, normalizeCollectionResult, normalizeGalleryImage, normalizeIconClass, parentBreakpoint, parseUrlList, partClassName, planDataSource, planResponsiveRules, referencedDataSources, referencedDataSourcesDeep, renderDefaultDeclarations, resetSeoField, resolveBoundValue, resolveContextValue, resolveDerivedInto, resolveGalleryEffect, resolveGalleryUrls, resolveInputChain, resolveInputs, resolvePageTransition, resolvePath, resolveRelativeInto, resolveRepeaterItems, resolveSeo, resolveSiblings, resolveSingleItemId, resolveStateStyles, resolveStyleProps, resolveValueChain, rewriteViewportUnits, safeSetInput, setInputOverride, setOverride, setSeoAuto, setSeoBinding, setSeoOverride, setSeoValue, stateNamesIn, stripSuppressedProps, styleKeyToKebab, tokensToDarkVarMap, tokensToVarMap, variableAliases, variableItemsFrom, withContextSlots, wrapInBreakpointQuery, wrapInMediaQuery };
2722
- export type { BoundValueSpec, BreakpointDef, BuildRelatedFilterOptions, ContextActionEffect, ContextActionLike, ContextItemRef, ContextSlotDefault, ContextValueSpec, ContextWriteScope, DataSourceDomain, DataSourceEndpoints, DataSourceMode, DataSourcePlan, DataSourceQueryFilter, DataSourceRequest, DataSourceRequestSpec, DataSourceRouteFilter, DataSourceSpec, DesignTokenLike, FormMessageResolver, GalleryBoundSource, GalleryDurations, GalleryEffect, GalleryEffectDescriptor, GalleryImage, GalleryLoopOptions, GalleryLoopScheduler, GalleryLoopState, GallerySlideState, IBuilderEventEmitter, InputMap, IoxContextSlot, IoxPipe, OverrideBreakpoint, PageStyleSettings, PageTransitionCategory, PageTransitionCategoryMeta, PageTransitionOptions, PageTransitionPreset, PipeArgSpec, PipeContext, PipeDescriptor, RelativeInputs, RelativeSourceKind, RelativeSourceSpec, ResolveSiblingsOptions, ResolvedPageTransition, ResponsiveLayer, ResponsiveOverrides, ResponsiveRuleSet, RouteContext, SeoDerivedContent, SeoFieldSource, SeoFieldState, SeoResolveContext, SetContextParams, SiblingsResult, StateStyleMap, StyleMap, StyleTreeNode, SwiperBreakpointMap, ToggleableAction, TransitionFeel, TransitionPhase, TransitionTimelineInput, ValueSource, VirtualTraitGroup };
2781
+ export { AOSService, BREAKPOINTS, Breakpoint, BreakpointQueryKind, BuilderButtonComponent, BuilderDividerComponent, BuilderHeadingComponent, BuilderIconComponent, BuilderImageComponent, BuilderLinkComponent, BuilderSpacerComponent, ButtonBlockComponent, CONTEXT_ACTION_TYPES, CardComponent, ClientGalleryComponent, ClientListComponent, ClientListItemComponent, ClientRepeaterComponent, ClientSliderContainerComponent, ClientSliderSlideComponent, ComponentInstanceRegistryService, ContainerComponent, ContextStore, DEFAULT_FLAVOR, DEFAULT_GALLERY_SCHEDULER, DEFAULT_PAGE_TRANSITION, DEFAULT_PAGE_TRANSITION_DURATION, DEFAULT_PAGE_TRANSITION_EASING, DEFAULT_PAGE_TRANSITION_PERSPECTIVE, DEFAULT_PHOSPHOR_WEIGHT, DEFAULT_WIDTH_BY_TYPE, DeclarationContext, FormRenderState, FormRuntimeService, GALLERY_EFFECTS, GALLERY_EFFECT_DESCRIPTORS, GalleryLoop, IOX_BUILDER_EVENTS, IOX_FORM_MESSAGES, IOX_FORM_NAVIGATOR, IOX_FORM_TRANSPORT, IS_PREVIEW, IoxAnimateContainer, IoxAosDirective, IoxAosModule, IoxBuilderComponentsModule, IoxComponentRegistryService, IoxFieldComponent, IoxFormComponent, IoxFormErrorComponent, IoxFormSuccessComponent, IoxPageComponent, IoxPageModule, IoxSubmitButtonComponent, IoxUiModule, LinkedContainerComponent, PAGE_TRANSITION_CATEGORIES, PAGE_TRANSITION_PRESETS, PART_CLASS_NAMES, PIPE_DESCRIPTORS, PIPE_REGISTRY, PageScrollProvider, RESERVED_ALIAS_PREFIX, SUPPRESSED_STYLE_PROPS_BY_TYPE, SWIPER_RESPONSIVE_TRAITS, SWISS_FLAVOR, SectionComponent, TRANSITION_FEELS, TextBlockComponent, VALIDATION_STATES, VALIDATION_STATE_CLASS, VIRTUAL_TRAIT_GROUPS, VIRTUAL_TRAIT_KEYS, ViewPositionDirective, ViewPositionModule, activeActions, applyBindingPipes, applyFeel, breakpointDef, breakpointForWidth, breakpointQueryFor, buildDataSourceFilter, buildPartSelector, buildRelatedFilter, buildSwiperBreakpoints, buildTargetedStateSelector, buildTransitionTimeline, cascadeChain, clearInputOverride, clearOverride, collectContextSlotKeys, collectReferencedAliases, compileDeclarations, compilePageStyle, compileTokenCss, composeVirtualTraits, computeRelative, contextActionEffect, contextSlotKeys, countOverrides, dataSourcePlanToRequest, declaredInputNames, defaultMessageResolver, deriveSeoFromContent, describeSeoFields, effectiveTransitionId, excludeSelf, feelFor, flavorDefaultDeclarations, getByPath, getSeoValue, initialSlotValues, isActionEnabled, isContextAction, isInputOverriddenAt, isOverriddenAt, isReservedAlias, isValidSlotKey, legacyEnterToTransition, matchRouteParams, mediaQueryFor, mergeSwiperBreakpoints, nearestCommonAncestorId, nodeCssId, nodeUsesContext, normalizeCollectionResult, normalizeGalleryImage, normalizeIconClass, parentBreakpoint, parseUrlList, partClassName, planDataSource, planResponsiveRules, referencedDataSources, referencedDataSourcesDeep, renderDefaultDeclarations, resetSeoField, resolveBoundValue, resolveContextValue, resolveDerivedInto, resolveFlavorDefaults, resolveGalleryEffect, resolveGalleryUrls, resolveInputChain, resolveInputs, resolvePageTransition, resolvePath, resolveRelativeInto, resolveRepeaterItems, resolveSeo, resolveSiblings, resolveSingleItemId, resolveStateStyles, resolveStyleProps, resolveValueChain, rewriteViewportUnits, safeSetInput, setInputOverride, setOverride, setSeoAuto, setSeoBinding, setSeoOverride, setSeoValue, stateNamesIn, stripSuppressedProps, styleKeyToKebab, tokensToDarkVarMap, tokensToVarMap, validateFlavorPack, variableAliases, variableItemsFrom, withContextSlots, wrapInBreakpointQuery, wrapInMediaQuery };
2782
+ export type { BoundValueSpec, BreakpointDef, BuildRelatedFilterOptions, ContextActionEffect, ContextActionLike, ContextItemRef, ContextSlotDefault, ContextValueSpec, ContextWriteScope, DataSourceDomain, DataSourceEndpoints, DataSourceMode, DataSourcePlan, DataSourceQueryFilter, DataSourceRequest, DataSourceRequestSpec, DataSourceRouteFilter, DataSourceSpec, DesignTokenLike, FlavorPack, FlavorTokenDef, FormMessageResolver, GalleryBoundSource, GalleryDurations, GalleryEffect, GalleryEffectDescriptor, GalleryImage, GalleryLoopOptions, GalleryLoopScheduler, GalleryLoopState, GallerySlideState, IBuilderEventEmitter, InputMap, IoxContextSlot, IoxPipe, OverrideBreakpoint, PageStyleSettings, PageTransitionCategory, PageTransitionCategoryMeta, PageTransitionOptions, PageTransitionPreset, PipeArgSpec, PipeContext, PipeDescriptor, RelativeInputs, RelativeSourceKind, RelativeSourceSpec, ResolveSiblingsOptions, ResolvedPageTransition, ResponsiveLayer, ResponsiveOverrides, ResponsiveRuleSet, RouteContext, SeoDerivedContent, SeoFieldSource, SeoFieldState, SeoResolveContext, SetContextParams, SiblingsResult, StateStyleMap, StyleMap, StyleTreeNode, SwiperBreakpointMap, ToggleableAction, TransitionFeel, TransitionPhase, TransitionTimelineInput, ValueSource, VirtualTraitGroup };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vectoriox/iox-ui",
3
- "version": "5.2.0",
3
+ "version": "5.4.0",
4
4
  "peerDependencies": {
5
5
  "@angular/common": ">=20.0.0",
6
6
  "@angular/core": ">=20.0.0",
@@ -23,6 +23,10 @@
23
23
  "types": "./actions-core/index.d.ts",
24
24
  "default": "./fesm2022/vectoriox-iox-ui-actions-core.mjs"
25
25
  },
26
+ "./consent-core": {
27
+ "types": "./consent-core/index.d.ts",
28
+ "default": "./fesm2022/vectoriox-iox-ui-consent-core.mjs"
29
+ },
26
30
  "./forms-core": {
27
31
  "types": "./forms-core/index.d.ts",
28
32
  "default": "./fesm2022/vectoriox-iox-ui-forms-core.mjs"
@@ -0,0 +1,38 @@
1
+ import { ConsentCategory, ConsentDecision, hasConsent } from './consent.model';
2
+
3
+ /**
4
+ * Google Consent Mode v2 (consent plan §4): four parameters, mandatory for EEA/UK measurement
5
+ * since March 2024. Our categories map onto them; `necessary` maps to none.
6
+ */
7
+ export type ConsentModeSignal = 'granted' | 'denied';
8
+
9
+ export const CONSENT_MODE_PARAMS = [
10
+ 'analytics_storage', 'ad_storage', 'ad_user_data', 'ad_personalization',
11
+ ] as const;
12
+
13
+ /** What the SSR head declares BEFORE any tag can load (invariant I1): everything denied. */
14
+ export const CONSENT_DEFAULTS_DENIED: Readonly<Record<(typeof CONSENT_MODE_PARAMS)[number], ConsentModeSignal>> =
15
+ Object.freeze({
16
+ analytics_storage: 'denied',
17
+ ad_storage: 'denied',
18
+ ad_user_data: 'denied',
19
+ ad_personalization: 'denied',
20
+ });
21
+
22
+ /** A decision → the four v2 signals. `null` (undecided) is indistinguishable from denied. */
23
+ export function toConsentModeSignals(decision: ConsentDecision | null | undefined): Record<string, ConsentModeSignal> {
24
+ const analytics: ConsentModeSignal = hasConsent(decision ?? null, ConsentCategory.Analytics) ? 'granted' : 'denied';
25
+ const marketing: ConsentModeSignal = hasConsent(decision ?? null, ConsentCategory.Marketing) ? 'granted' : 'denied';
26
+ return {
27
+ analytics_storage: analytics,
28
+ ad_storage: marketing,
29
+ ad_user_data: marketing,
30
+ ad_personalization: marketing,
31
+ };
32
+ }
33
+
34
+ /** The inline `<head>` bootstrap (basic mode, D-C2): defaults denied before anything loads. */
35
+ export function consentModeBootstrapJs(): string {
36
+ return "window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments);}" +
37
+ `gtag('consent','default',${JSON.stringify(CONSENT_DEFAULTS_DENIED)});`;
38
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * The visitor-consent vocabulary (consent plan C1). Framework-free for the same forced reason as
3
+ * forms-core/seo-core/actions-core: iox-cms consumes it under plain Node, the engine at SSR time,
4
+ * the builder for design-time preview — ONE implementation of what a decision is.
5
+ */
6
+
7
+ export enum ConsentCategory {
8
+ /** Always on, never user-controllable — the site working at all. */
9
+ Necessary = 'necessary',
10
+ Analytics = 'analytics',
11
+ Marketing = 'marketing',
12
+ }
13
+
14
+ /** A visitor's recorded decision. `necessary` is implied true and never stored as false. */
15
+ export interface ConsentDecision {
16
+ categories: Partial<Record<ConsentCategory, boolean>>;
17
+ /** Bumping the org's policy version invalidates every prior decision. */
18
+ policyVersion: string;
19
+ /** ISO timestamp of the decision. */
20
+ decidedAt: string;
21
+ anonymousId: string;
22
+ }
23
+
24
+ /** A fresh "nothing granted" decision skeleton — used when a visitor must be asked. */
25
+ export function emptyDecision(anonymousId: string, policyVersion: string, now: Date = new Date()): ConsentDecision {
26
+ return {
27
+ categories: { [ConsentCategory.Necessary]: true },
28
+ policyVersion,
29
+ decidedAt: now.toISOString(),
30
+ anonymousId,
31
+ };
32
+ }
33
+
34
+ /** `null` (no decision yet) grants nothing; `necessary` is always granted. */
35
+ export function hasConsent(decision: ConsentDecision | null | undefined, category: ConsentCategory): boolean {
36
+ if (category === ConsentCategory.Necessary) return true;
37
+ if (!decision) return false;
38
+ return decision.categories?.[category] === true;
39
+ }
40
+
41
+ /** A decision older than `maxMonths` no longer counts (default re-ask: 12 months, D-C6). */
42
+ export function isExpired(decision: ConsentDecision, now: Date, maxMonths: number): boolean {
43
+ const decided = new Date(decision.decidedAt);
44
+ if (Number.isNaN(decided.getTime())) return true;
45
+ const expiry = new Date(decided);
46
+ expiry.setMonth(expiry.getMonth() + maxMonths);
47
+ return now >= expiry;
48
+ }
49
+
50
+ /**
51
+ * The function the dead fork got wrong: **a recorded "no" is a decision.** The banner shows only
52
+ * when there is no decision, the decision expired, or the policy version moved on — never because
53
+ * the visitor declined.
54
+ */
55
+ export function needsPrompt(
56
+ decision: ConsentDecision | null | undefined,
57
+ policyVersion: string,
58
+ now: Date = new Date(),
59
+ maxMonths = 12,
60
+ ): boolean {
61
+ if (!decision) return true;
62
+ if (decision.policyVersion !== policyVersion) return true;
63
+ return isExpired(decision, now, maxMonths);
64
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * @vectoriox/iox-ui/consent-core — the shared, FRAMEWORK-FREE core of visitor consent.
3
+ *
4
+ * What a decision IS (`needsPrompt`: a recorded "no" is a decision), how it travels (a tolerant
5
+ * first-party cookie), and what it tells Google (Consent Mode v2 signals, denied-by-default).
6
+ * Consumed by the SSR engine (the gate, C2), the builder (banner preview, C3) and iox-cms (the
7
+ * optional per-org log, C4) — the fourth entry point on the forms-core pattern, replacing the
8
+ * dead byte-identical fork C0 deleted.
9
+ * ⚠️ Consumers wire the usual trio — ng-package raw-ts assets, jest moduleNameMapper, tsconfig
10
+ * paths — or `nest start` dies with ERR_REQUIRE_ESM while jest and build both pass.
11
+ */
12
+ export * from './consent.model';
13
+ export * from './storage';
14
+ export * from './consent-mode';
@@ -0,0 +1,43 @@
1
+ import { ConsentCategory, ConsentDecision } from './consent.model';
2
+
3
+ /** First-party cookie name — the visitor's browser carries the proof (D-C1). */
4
+ export const CONSENT_COOKIE_NAME = 'iox-consent';
5
+
6
+ /** Cookie payload. Versioned so a future shape change can read old cookies tolerantly. */
7
+ export function encode(decision: ConsentDecision): string {
8
+ return JSON.stringify({ v: 1, ...decision });
9
+ }
10
+
11
+ /**
12
+ * Tolerant by contract: bad JSON, a foreign shape, a tampered cookie — all read as `null`
13
+ * ("no decision"), never a throw and never a guess. The old fork threw its way to a re-prompt.
14
+ */
15
+ export function decode(raw: string | null | undefined): ConsentDecision | null {
16
+ if (!raw) return null;
17
+ try {
18
+ const parsed = JSON.parse(raw);
19
+ if (!parsed || typeof parsed !== 'object') return null;
20
+ if (typeof parsed.anonymousId !== 'string' || !parsed.anonymousId) return null;
21
+ if (typeof parsed.policyVersion !== 'string') return null;
22
+ if (!parsed.categories || typeof parsed.categories !== 'object' || Array.isArray(parsed.categories)) return null;
23
+ const categories: Partial<Record<ConsentCategory, boolean>> = {};
24
+ for (const [k, v] of Object.entries(parsed.categories)) {
25
+ categories[k as ConsentCategory] = v === true;
26
+ }
27
+ categories[ConsentCategory.Necessary] = true;
28
+ return {
29
+ categories,
30
+ policyVersion: parsed.policyVersion,
31
+ decidedAt: typeof parsed.decidedAt === 'string' ? parsed.decidedAt : '',
32
+ anonymousId: parsed.anonymousId,
33
+ };
34
+ } catch {
35
+ return null;
36
+ }
37
+ }
38
+
39
+ /** `Set-Cookie` attributes for the decision: a NECESSARY cookie, 12 months, first-party. */
40
+ export function cookieAttributes(maxMonths = 12): string {
41
+ const maxAge = Math.floor(maxMonths * 30.44 * 24 * 60 * 60);
42
+ return `Path=/; Max-Age=${maxAge}; SameSite=Lax; Secure`;
43
+ }