c15t 2.0.0-rc.8 → 2.0.4

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/CHANGELOG.md CHANGED
@@ -1,5 +1,39 @@
1
1
  # c15t
2
2
 
3
+ ## 2.0.4
4
+
5
+ ### Patch Changes
6
+
7
+ - 748536a: Refine policy category scope handling.
8
+ - Updated dependencies [748536a]
9
+ - @c15t/schema@2.0.1
10
+
11
+ ## 2.0.0
12
+
13
+ ### Major Changes
14
+
15
+ - 32617c9: Changelog available at https://c15t.com/changelog/2026-04-14-v2.0.0
16
+
17
+ ### Patch Changes
18
+
19
+ - Updated dependencies [32617c9]
20
+ - Updated dependencies [32617c9]
21
+ - @c15t/schema@2.0.0
22
+ - @c15t/translations@2.0.0
23
+
24
+ ## 2.0.0-rc.10
25
+
26
+ ### Patch Changes
27
+
28
+ - 9579b62: Add token-first legal-document consent groundwork for `2.0`.
29
+
30
+ - `c15t`: expand the unstable policy-consent input types so legal-document writes can prefer `documentSnapshotToken`, fall back to `policyHash`, and keep `policyId` only as a compatibility path.
31
+ - `@c15t/backend`: update legal-document consent writes to resolve append-only consent against a verified document snapshot token when configured, or against a provided document hash when only lighter-weight release proof is available.
32
+ - `@c15t/schema`: extend the subject consent schema and error shapes for legal-document snapshot tokens and hash-based legal-document resolution.
33
+
34
+ - Updated dependencies [9579b62]
35
+ - @c15t/schema@2.0.0-rc.6
36
+
3
37
  ## 2.0.0-rc.8
4
38
 
5
39
  ### Minor Changes
@@ -171,7 +205,7 @@
171
205
 
172
206
  ### Major Changes
173
207
 
174
- - 126a78b: https://v2.c15t.com/changelog/2026-02-12-v2.0.0-rc.0
208
+ - 126a78b: https://c15t.com/changelog/2026-02-12-v2.0.0-rc.0
175
209
 
176
210
  ### Patch Changes
177
211
 
package/README.md CHANGED
@@ -1,24 +1,24 @@
1
1
  <p align="center">
2
- <a href="https://c15t.com?utm_source=github&utm_medium=repopage_c15t" target="_blank" rel="noopener noreferrer">
2
+ <a href="https://c15t.com?utm_source=npm&utm_medium=readme&utm_campaign=oss_readme&utm_content=c15t" target="_blank" rel="noopener noreferrer">
3
3
  <picture>
4
4
  <source media="(prefers-color-scheme: dark)" srcset="../../docs/assets/c15t-banner-readme-dark.svg" type="image/svg+xml">
5
5
  <img src="../../docs/assets/c15t-banner-readme-light.svg" alt="c15t Banner" type="image/svg+xml">
6
6
  </picture>
7
7
  </a>
8
- <br />
9
- <h1 align="center">c15t: Developer-First Consent Management Platform</h1>
10
8
  </p>
11
9
 
10
+ # c15t: Developer-First Consent Management Platform
11
+
12
12
  [![GitHub stars](https://img.shields.io/github/stars/c15t/c15t?style=flat-square)](https://github.com/c15t/c15t)
13
13
  [![CI](https://img.shields.io/github/actions/workflow/status/c15t/c15t/ci.yml?style=flat-square)](https://github.com/c15t/c15t/actions/workflows/ci.yml)
14
- [![License](https://img.shields.io/badge/license-GPL--3.0-blue.svg?style=flat-square)](https://github.com/c15t/c15t/blob/main/LICENSE.md)
14
+ [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg?style=flat-square)](https://github.com/c15t/c15t/blob/main/LICENSE.md)
15
15
  [![Discord](https://img.shields.io/discord/1312171102268690493?style=flat-square)](https://c15t.link/discord)
16
16
  [![npm version](https://img.shields.io/npm/v/c15t?style=flat-square)](https://www.npmjs.com/package/c15t)
17
17
  [![Top Language](https://img.shields.io/github/languages/top/c15t/c15t?style=flat-square)](https://github.com/c15t/c15t)
18
18
  [![Last Commit](https://img.shields.io/github/last-commit/c15t/c15t?style=flat-square)](https://github.com/c15t/c15t/commits/main)
19
19
  [![Open Issues](https://img.shields.io/github/issues/c15t/c15t?style=flat-square)](https://github.com/c15t/c15t/issues)
20
20
 
21
- Developer-first CMP for JavaScript: cookie banner, consent manager, preferences centre. GDPR ready with minimal setup and rich customization
21
+ Headless cookie banner, consent manager & preference center for JavaScript / TypeScript. GDPR, CCPA, LGPD and IAB TCF compliant.
22
22
 
23
23
  ## Key Features
24
24
 
@@ -34,18 +34,18 @@ Developer-first CMP for JavaScript: cookie banner, consent manager, preferences
34
34
  - JavaScript or TypeScript project
35
35
  - Node.js 18.17.0 or later
36
36
  - npm, pnpm, or yarn package manager
37
- - A hosted [c15t instance](https://consent.io) (free sign-up) or [self-hosted deployment](https://c15t.com/docs/self-host/v2)
37
+ - A hosted [c15t instance](https://inth.com) (free sign-up) or [self-hosted deployment](https://c15t.com/docs/self-host/v2)
38
38
 
39
39
  ## Quick Start
40
40
 
41
41
  Easiest setup with @c15t/cli:
42
42
 
43
43
  ```bash
44
- # Generate schema and code
45
- pnpm dlx @c15t/cli generate
44
+ # Set up c15t in your project
45
+ pnpm dlx @c15t/cli setup
46
46
  # Alternatives:
47
- # npx @c15t/cli generate
48
- # bunx --bun @c15t/cli generate
47
+ # npx @c15t/cli setup
48
+ # bunx --bun @c15t/cli setup
49
49
  ```
50
50
 
51
51
  The CLI will:
@@ -88,24 +88,24 @@ For further information, guides, and examples visit the [reference documentation
88
88
 
89
89
  - Join our [Discord community](https://c15t.link/discord)
90
90
  - Open an issue on our [GitHub repository](https://github.com/c15t/c15t/issues)
91
- - Visit [consent.io](https://consent.io) and use the chat widget
92
- - Contact our support team via email [support@consent.io](mailto:support@consent.io)
91
+ - Visit [inth.com](https://inth.com) and use the chat widget
92
+ - Contact our support team via email [support@inth.com](mailto:support@inth.com)
93
93
 
94
94
  ## Contributing
95
95
 
96
- - We're open to all community contributions!
96
+ - We're open to all community contributions.
97
97
  - Read our [Contribution Guidelines](https://c15t.com/docs/oss/contributing)
98
98
  - Review our [Code of Conduct](https://c15t.com/docs/oss/code-of-conduct)
99
99
  - Fork the repository
100
100
  - Create a new branch for your feature
101
101
  - Submit a pull request
102
- - **All contributions, big or small, are welcome and appreciated!**
102
+ - **All contributions, big or small, are welcome and appreciated.**
103
103
 
104
104
  ## Security
105
105
 
106
106
  If you believe you have found a security vulnerability in c15t, we encourage you to **_responsibly disclose this and NOT open a public issue_**. We will investigate all legitimate reports.
107
107
 
108
- Our preference is that you make use of GitHub's private vulnerability reporting feature to disclose potential security vulnerabilities in our Open Source Software. To do this, please visit [https://github.com/c15t/c15t/security](https://github.com/c15t/c15t/security) and click the "Report a vulnerability" button.
108
+ Our preference is that you make use of GitHub's private vulnerability reporting feature to disclose potential security vulnerabilities in our open-source software. To do this, please visit [https://github.com/c15t/c15t/security](https://github.com/c15t/c15t/security) and click the "Report a vulnerability" button.
109
109
 
110
110
  ### Security Policy
111
111
 
@@ -116,8 +116,8 @@ Our preference is that you make use of GitHub's private vulnerability reporting
116
116
 
117
117
  ## License
118
118
 
119
- [GNU General Public License v3.0](https://github.com/c15t/c15t/blob/main/LICENSE.md)
119
+ [Apache License 2.0](https://github.com/c15t/c15t/blob/main/LICENSE.md)
120
120
 
121
121
  ---
122
122
 
123
- **Built with ❤️ by the [consent.io](https://www.consent.io?utm_source=github&utm_medium=repopage_c15t) team**
123
+ **Built by [Inth](https://inth.com?utm_source=npm&utm_medium=readme&utm_campaign=oss_readme&utm_content=c15t)**
package/dist/index.cjs CHANGED
@@ -601,14 +601,13 @@ const consent_types_consentTypes = [
601
601
  }
602
602
  ];
603
603
  const allConsentNames = consent_types_consentTypes.map((consent)=>consent.name);
604
- const version = '2.0.0-rc.8';
605
604
  const STORAGE_KEY_V2 = 'c15t';
606
605
  const STORAGE_KEY = 'privacy-consent-storage';
607
606
  const initial_state_initialState = {
608
607
  debug: false,
609
608
  config: {
610
609
  pkg: 'c15t',
611
- version: version,
610
+ version: "2.0.4",
612
611
  mode: 'Unknown'
613
612
  },
614
613
  consents: consent_types_consentTypes.reduce((acc, consent)=>{
@@ -1768,11 +1767,24 @@ function flattenLayout(layout) {
1768
1767
  }
1769
1768
  function applyPolicyPurposeAllowlist(preferences, allowedPurposeIds) {
1770
1769
  if (!allowedPurposeIds || 0 === allowedPurposeIds.length || allowedPurposeIds.includes('*')) return preferences;
1771
- const allowed = new Set(allowedPurposeIds);
1770
+ const allowed = new Set([
1771
+ 'necessary',
1772
+ ...allowedPurposeIds
1773
+ ]);
1772
1774
  const next = {};
1773
1775
  for (const [key, value] of Object.entries(preferences))next[key] = allowed.has(key) ? value : false;
1774
1776
  return next;
1775
1777
  }
1778
+ function stripDisallowedPreferenceKeys(preferences, allowedPurposeIds) {
1779
+ if (!allowedPurposeIds || 0 === allowedPurposeIds.length || allowedPurposeIds.includes('*')) return preferences;
1780
+ const allowed = new Set([
1781
+ 'necessary',
1782
+ ...allowedPurposeIds
1783
+ ]);
1784
+ const next = {};
1785
+ for (const [key, value] of Object.entries(preferences))if (allowed.has(key)) next[key] = value;
1786
+ return next;
1787
+ }
1776
1788
  function filterConsentCategoriesByPolicy(categories, allowedPurposeIds) {
1777
1789
  const uniqueCategories = Array.from(new Set(categories));
1778
1790
  if (!allowedPurposeIds || 0 === allowedPurposeIds.length || allowedPurposeIds.includes('*')) return uniqueCategories;
@@ -1784,19 +1796,11 @@ function filterConsentCategoriesByPolicy(categories, allowedPurposeIds) {
1784
1796
  if (!filtered.includes('necessary')) filtered.unshift('necessary');
1785
1797
  return filtered;
1786
1798
  }
1787
- function applyPolicyScopeForRuntimeGating(consents, allowedPurposeIds, scopeMode = 'permissive') {
1788
- if ('strict' === scopeMode) return consents;
1789
- if (!allowedPurposeIds || 0 === allowedPurposeIds.length || allowedPurposeIds.includes('*')) return consents;
1790
- const allowedCategories = new Set([
1791
- 'necessary',
1792
- ...allowedPurposeIds.filter(isConsentCategory)
1793
- ]);
1794
- const next = {
1795
- ...consents
1796
- };
1797
- for (const category of allConsentNames)if (!allowedCategories.has(category)) next[category] = true;
1798
- next.necessary = true;
1799
- return next;
1799
+ function shouldEnforcePolicyCategoryScope(allowedPurposeIds, scopeMode = 'permissive') {
1800
+ return 'strict' === scopeMode && Array.isArray(allowedPurposeIds) && allowedPurposeIds.length > 0 && !allowedPurposeIds.includes('*');
1801
+ }
1802
+ function applyPolicyScopeForRuntimeGating(consents, _allowedPurposeIds, _scopeMode = 'permissive') {
1803
+ return consents;
1800
1804
  }
1801
1805
  function getEffectivePolicy(initData) {
1802
1806
  return initData?.policy;
@@ -2799,8 +2803,11 @@ async function saveConsents({ manager, type, get, set, options, emitConsentChang
2799
2803
  if ('all' === type) {
2800
2804
  for (const consent of consentTypes)if (consentCategories.includes(consent.name)) newConsents[consent.name] = true;
2801
2805
  } else if ('necessary' === type) for (const consent of consentTypes)newConsents[consent.name] = true === consent.disabled ? consent.defaultValue : false;
2802
- const policyCategories = getEffectivePolicy(lastBannerFetchData)?.consent?.categories;
2803
- const effectiveConsents = applyPolicyPurposeAllowlist(newConsents, policyCategories);
2806
+ const effectivePolicy = getEffectivePolicy(lastBannerFetchData);
2807
+ const policyCategories = effectivePolicy?.consent?.categories;
2808
+ const shouldEnforcePolicyScope = shouldEnforcePolicyCategoryScope(policyCategories, effectivePolicy?.consent?.scopeMode ?? null);
2809
+ const effectiveConsents = shouldEnforcePolicyScope ? applyPolicyPurposeAllowlist(newConsents, policyCategories) : newConsents;
2810
+ const requestPreferences = shouldEnforcePolicyScope ? stripDisallowedPreferenceKeys(effectiveConsents, policyCategories) : effectiveConsents;
2804
2811
  const didChange = haveConsentsChanged(previousConsents, effectiveConsents, consentTypes);
2805
2812
  const nextConsentCategoryLists = getConsentCategoryLists(effectiveConsents, consentCategories, consentTypes);
2806
2813
  const previousConsentCategoryLists = getConsentCategoryLists(previousConsents, consentCategories, consentTypes);
@@ -2851,7 +2858,7 @@ async function saveConsents({ manager, type, get, set, options, emitConsentChang
2851
2858
  const pendingSync = {
2852
2859
  type,
2853
2860
  subjectId,
2854
- preferences: effectiveConsents,
2861
+ preferences: requestPreferences,
2855
2862
  givenAt,
2856
2863
  jurisdiction: locationInfo?.jurisdiction ?? void 0,
2857
2864
  jurisdictionModel: model,
@@ -2890,7 +2897,7 @@ async function saveConsents({ manager, type, get, set, options, emitConsentChang
2890
2897
  body: {
2891
2898
  type: 'cookie_banner',
2892
2899
  domain: window.location.hostname,
2893
- preferences: effectiveConsents,
2900
+ preferences: requestPreferences,
2894
2901
  subjectId,
2895
2902
  jurisdiction: locationInfo?.jurisdiction ?? void 0,
2896
2903
  jurisdictionModel: model ?? void 0,
@@ -3001,8 +3008,8 @@ function buildStoreUpdate(data, config, effectiveIABEnabled, initSourceMetadata)
3001
3008
  update.selectedConsents = autoGrantedConsents;
3002
3009
  }
3003
3010
  const policyCategories = data.policy?.consent?.categories;
3004
- const hasPolicyCategoryAllowlist = Array.isArray(policyCategories) && policyCategories.length > 0 && !policyCategories.includes('*');
3005
- if (hasPolicyCategoryAllowlist) {
3011
+ const hasStrictPolicyCategoryAllowlist = shouldEnforcePolicyCategoryScope(policyCategories, data.policy?.consent?.scopeMode ?? null);
3012
+ if (hasStrictPolicyCategoryAllowlist) {
3006
3013
  const uniqueAllowedCategories = filterConsentCategoriesByPolicy(allConsentNames, policyCategories);
3007
3014
  update.consentCategories = uniqueAllowedCategories;
3008
3015
  update.consents = applyPolicyPurposeAllowlist(update.consents ?? get().consents, uniqueAllowedCategories);
@@ -3011,7 +3018,8 @@ function buildStoreUpdate(data, config, effectiveIABEnabled, initSourceMetadata)
3011
3018
  const preselectedCategories = data.policy?.consent?.preselectedCategories;
3012
3019
  const shouldApplyPreselectedCategories = null === consentInfo && !autoGrantedConsents && Array.isArray(preselectedCategories) && preselectedCategories.length > 0;
3013
3020
  if (shouldApplyPreselectedCategories) {
3014
- const preselectedScope = hasPolicyCategoryAllowlist ? filterConsentCategoriesByPolicy(allConsentNames, policyCategories) : allConsentNames;
3021
+ const displayedConsentNames = update.consentCategories ?? get().consentCategories;
3022
+ const preselectedScope = hasStrictPolicyCategoryAllowlist ? filterConsentCategoriesByPolicy(displayedConsentNames, policyCategories) : displayedConsentNames;
3015
3023
  const allowedPreselectedCategories = filterConsentCategoriesByPolicy(preselectedScope, preselectedCategories);
3016
3024
  const preselectedSet = new Set(allowedPreselectedCategories);
3017
3025
  const selectedConsentBaseline = update.selectedConsents ?? get().selectedConsents;
@@ -3724,8 +3732,14 @@ const createConsentManagerStore = (manager, options = {})=>{
3724
3732
  return resetState;
3725
3733
  });
3726
3734
  },
3727
- setConsentCategories: (types)=>set({
3728
- consentCategories: filterConsentCategoriesByPolicy(types, get().policyCategories)
3735
+ setConsentCategories: (types)=>set(()=>{
3736
+ const { policyCategories, policyScopeMode } = get();
3737
+ if (shouldEnforcePolicyCategoryScope(policyCategories, policyScopeMode)) return {
3738
+ consentCategories: filterConsentCategoriesByPolicy(types, policyCategories)
3739
+ };
3740
+ return {
3741
+ consentCategories: Array.from(new Set(types))
3742
+ };
3729
3743
  }),
3730
3744
  setCallback: (name, callback)=>{
3731
3745
  const currentState = get();
@@ -3797,13 +3811,15 @@ const createConsentManagerStore = (manager, options = {})=>{
3797
3811
  });
3798
3812
  },
3799
3813
  updateConsentCategories: (newCategories)=>{
3814
+ const { consentCategories: currentConsentCategories, policyCategories, policyScopeMode } = get();
3800
3815
  const allCategoriesSet = new Set([
3801
- ...get().consentCategories,
3816
+ ...currentConsentCategories,
3802
3817
  ...newCategories
3803
3818
  ]);
3804
- const allCategories = filterConsentCategoriesByPolicy(Array.from(allCategoriesSet), get().policyCategories);
3819
+ let consentCategories;
3820
+ consentCategories = shouldEnforcePolicyCategoryScope(policyCategories, policyScopeMode) ? filterConsentCategoriesByPolicy(Array.from(allCategoriesSet), policyCategories) : Array.from(allCategoriesSet);
3805
3821
  set({
3806
- consentCategories: allCategories
3822
+ consentCategories
3807
3823
  });
3808
3824
  },
3809
3825
  identifyUser: async (user)=>{
@@ -3831,6 +3847,103 @@ const createConsentManagerStore = (manager, options = {})=>{
3831
3847
  }
3832
3848
  });
3833
3849
  },
3850
+ unstable_acceptPolicyConsent: async (input)=>{
3851
+ const currentState = get();
3852
+ const currentInfo = currentState.consentInfo;
3853
+ const subjectId = currentInfo?.subjectId ?? generateSubjectId();
3854
+ const storedIdentifiers = sanitizeSubjectIdentifiers({
3855
+ externalId: currentInfo?.externalId,
3856
+ identityProvider: currentInfo?.identityProvider
3857
+ });
3858
+ const userIdentifiers = sanitizeSubjectIdentifiers({
3859
+ externalId: currentState.user?.id,
3860
+ identityProvider: currentState.user?.identityProvider
3861
+ });
3862
+ const inputIdentifiers = sanitizeSubjectIdentifiers({
3863
+ externalId: input.externalId,
3864
+ identityProvider: input.identityProvider
3865
+ });
3866
+ const externalId = inputIdentifiers.externalId ?? storedIdentifiers.externalId ?? userIdentifiers.externalId;
3867
+ const identityProvider = inputIdentifiers.identityProvider ?? storedIdentifiers.identityProvider ?? userIdentifiers.identityProvider;
3868
+ const givenAt = input.givenAt ?? Date.now();
3869
+ const domain = input.domain ?? ("u" > typeof window ? window.location.hostname : 'localhost');
3870
+ const isLegalDocumentType = 'privacy_policy' === input.type || 'terms_and_conditions' === input.type || 'dpa' === input.type;
3871
+ let legalDocumentFields = {};
3872
+ if (isLegalDocumentType) if (input.documentSnapshotToken) legalDocumentFields = {
3873
+ documentSnapshotToken: input.documentSnapshotToken
3874
+ };
3875
+ else if (input.policyHash) legalDocumentFields = {
3876
+ policyHash: input.policyHash
3877
+ };
3878
+ else if (input.policyId) legalDocumentFields = {
3879
+ policyId: input.policyId
3880
+ };
3881
+ else throw new Error('Legal document consent requires documentSnapshotToken, policyHash, or policyId.');
3882
+ const response = await manager.setConsent({
3883
+ body: {
3884
+ type: input.type,
3885
+ subjectId,
3886
+ domain,
3887
+ givenAt,
3888
+ uiSource: input.uiSource ?? 'api',
3889
+ ...legalDocumentFields,
3890
+ ...input.metadata ? {
3891
+ metadata: input.metadata
3892
+ } : {},
3893
+ ...input.preferences ? {
3894
+ preferences: input.preferences
3895
+ } : {},
3896
+ ...externalId ? {
3897
+ externalSubjectId: externalId
3898
+ } : {},
3899
+ ...identityProvider ? {
3900
+ identityProvider
3901
+ } : {}
3902
+ }
3903
+ });
3904
+ if (!response.ok || !response.data) {
3905
+ const errorMsg = response.error?.message ?? 'Failed to accept policy consent';
3906
+ get().callbacks.onError?.({
3907
+ error: errorMsg
3908
+ });
3909
+ const error = new Error(errorMsg);
3910
+ error.code = response.error?.code;
3911
+ error.details = response.error?.details ?? null;
3912
+ error.status = response.error?.status;
3913
+ throw error;
3914
+ }
3915
+ const consent = {
3916
+ ...response.data,
3917
+ givenAt: response.data.givenAt instanceof Date ? response.data.givenAt : new Date(response.data.givenAt)
3918
+ };
3919
+ const latestState = get();
3920
+ const latestInfo = latestState.consentInfo;
3921
+ const nextConsentInfo = {
3922
+ ...latestInfo,
3923
+ time: givenAt,
3924
+ subjectId,
3925
+ ...externalId ? {
3926
+ externalId
3927
+ } : {},
3928
+ ...identityProvider ? {
3929
+ identityProvider
3930
+ } : {}
3931
+ };
3932
+ set({
3933
+ consentInfo: nextConsentInfo,
3934
+ ...externalId ? {
3935
+ user: {
3936
+ id: externalId,
3937
+ identityProvider
3938
+ }
3939
+ } : {}
3940
+ });
3941
+ saveConsentToStorage({
3942
+ consents: latestState.consents,
3943
+ consentInfo: nextConsentInfo
3944
+ }, void 0, latestState.storageConfig);
3945
+ return consent;
3946
+ },
3834
3947
  setOverrides: async (overrides)=>{
3835
3948
  set({
3836
3949
  overrides: {
@@ -3960,7 +4073,7 @@ function getOrCreateConsentRuntime(options, pkgInfo) {
3960
4073
  config: {
3961
4074
  ...userConfig ?? {},
3962
4075
  pkg: pkgInfo?.pkg || 'c15t',
3963
- version: pkgInfo?.version || version,
4076
+ version: pkgInfo?.version || "2.0.4",
3964
4077
  mode: normalizedMode,
3965
4078
  meta: {
3966
4079
  ...userConfig?.meta ?? {},
package/dist/index.js CHANGED
@@ -525,14 +525,13 @@ const consent_types_consentTypes = [
525
525
  }
526
526
  ];
527
527
  const allConsentNames = consent_types_consentTypes.map((consent)=>consent.name);
528
- const version = '2.0.0-rc.8';
529
528
  const STORAGE_KEY_V2 = 'c15t';
530
529
  const STORAGE_KEY = 'privacy-consent-storage';
531
530
  const initial_state_initialState = {
532
531
  debug: false,
533
532
  config: {
534
533
  pkg: 'c15t',
535
- version: version,
534
+ version: "2.0.4",
536
535
  mode: 'Unknown'
537
536
  },
538
537
  consents: consent_types_consentTypes.reduce((acc, consent)=>{
@@ -1692,11 +1691,24 @@ function flattenLayout(layout) {
1692
1691
  }
1693
1692
  function applyPolicyPurposeAllowlist(preferences, allowedPurposeIds) {
1694
1693
  if (!allowedPurposeIds || 0 === allowedPurposeIds.length || allowedPurposeIds.includes('*')) return preferences;
1695
- const allowed = new Set(allowedPurposeIds);
1694
+ const allowed = new Set([
1695
+ 'necessary',
1696
+ ...allowedPurposeIds
1697
+ ]);
1696
1698
  const next = {};
1697
1699
  for (const [key, value] of Object.entries(preferences))next[key] = allowed.has(key) ? value : false;
1698
1700
  return next;
1699
1701
  }
1702
+ function stripDisallowedPreferenceKeys(preferences, allowedPurposeIds) {
1703
+ if (!allowedPurposeIds || 0 === allowedPurposeIds.length || allowedPurposeIds.includes('*')) return preferences;
1704
+ const allowed = new Set([
1705
+ 'necessary',
1706
+ ...allowedPurposeIds
1707
+ ]);
1708
+ const next = {};
1709
+ for (const [key, value] of Object.entries(preferences))if (allowed.has(key)) next[key] = value;
1710
+ return next;
1711
+ }
1700
1712
  function filterConsentCategoriesByPolicy(categories, allowedPurposeIds) {
1701
1713
  const uniqueCategories = Array.from(new Set(categories));
1702
1714
  if (!allowedPurposeIds || 0 === allowedPurposeIds.length || allowedPurposeIds.includes('*')) return uniqueCategories;
@@ -1708,19 +1720,11 @@ function filterConsentCategoriesByPolicy(categories, allowedPurposeIds) {
1708
1720
  if (!filtered.includes('necessary')) filtered.unshift('necessary');
1709
1721
  return filtered;
1710
1722
  }
1711
- function applyPolicyScopeForRuntimeGating(consents, allowedPurposeIds, scopeMode = 'permissive') {
1712
- if ('strict' === scopeMode) return consents;
1713
- if (!allowedPurposeIds || 0 === allowedPurposeIds.length || allowedPurposeIds.includes('*')) return consents;
1714
- const allowedCategories = new Set([
1715
- 'necessary',
1716
- ...allowedPurposeIds.filter(isConsentCategory)
1717
- ]);
1718
- const next = {
1719
- ...consents
1720
- };
1721
- for (const category of allConsentNames)if (!allowedCategories.has(category)) next[category] = true;
1722
- next.necessary = true;
1723
- return next;
1723
+ function shouldEnforcePolicyCategoryScope(allowedPurposeIds, scopeMode = 'permissive') {
1724
+ return 'strict' === scopeMode && Array.isArray(allowedPurposeIds) && allowedPurposeIds.length > 0 && !allowedPurposeIds.includes('*');
1725
+ }
1726
+ function applyPolicyScopeForRuntimeGating(consents, _allowedPurposeIds, _scopeMode = 'permissive') {
1727
+ return consents;
1724
1728
  }
1725
1729
  function getEffectivePolicy(initData) {
1726
1730
  return initData?.policy;
@@ -2722,8 +2726,11 @@ async function saveConsents({ manager, type, get, set, options, emitConsentChang
2722
2726
  if ('all' === type) {
2723
2727
  for (const consent of consentTypes)if (consentCategories.includes(consent.name)) newConsents[consent.name] = true;
2724
2728
  } else if ('necessary' === type) for (const consent of consentTypes)newConsents[consent.name] = true === consent.disabled ? consent.defaultValue : false;
2725
- const policyCategories = getEffectivePolicy(lastBannerFetchData)?.consent?.categories;
2726
- const effectiveConsents = applyPolicyPurposeAllowlist(newConsents, policyCategories);
2729
+ const effectivePolicy = getEffectivePolicy(lastBannerFetchData);
2730
+ const policyCategories = effectivePolicy?.consent?.categories;
2731
+ const shouldEnforcePolicyScope = shouldEnforcePolicyCategoryScope(policyCategories, effectivePolicy?.consent?.scopeMode ?? null);
2732
+ const effectiveConsents = shouldEnforcePolicyScope ? applyPolicyPurposeAllowlist(newConsents, policyCategories) : newConsents;
2733
+ const requestPreferences = shouldEnforcePolicyScope ? stripDisallowedPreferenceKeys(effectiveConsents, policyCategories) : effectiveConsents;
2727
2734
  const didChange = haveConsentsChanged(previousConsents, effectiveConsents, consentTypes);
2728
2735
  const nextConsentCategoryLists = getConsentCategoryLists(effectiveConsents, consentCategories, consentTypes);
2729
2736
  const previousConsentCategoryLists = getConsentCategoryLists(previousConsents, consentCategories, consentTypes);
@@ -2774,7 +2781,7 @@ async function saveConsents({ manager, type, get, set, options, emitConsentChang
2774
2781
  const pendingSync = {
2775
2782
  type,
2776
2783
  subjectId,
2777
- preferences: effectiveConsents,
2784
+ preferences: requestPreferences,
2778
2785
  givenAt,
2779
2786
  jurisdiction: locationInfo?.jurisdiction ?? void 0,
2780
2787
  jurisdictionModel: model,
@@ -2813,7 +2820,7 @@ async function saveConsents({ manager, type, get, set, options, emitConsentChang
2813
2820
  body: {
2814
2821
  type: 'cookie_banner',
2815
2822
  domain: window.location.hostname,
2816
- preferences: effectiveConsents,
2823
+ preferences: requestPreferences,
2817
2824
  subjectId,
2818
2825
  jurisdiction: locationInfo?.jurisdiction ?? void 0,
2819
2826
  jurisdictionModel: model ?? void 0,
@@ -2924,8 +2931,8 @@ function buildStoreUpdate(data, config, effectiveIABEnabled, initSourceMetadata)
2924
2931
  update.selectedConsents = autoGrantedConsents;
2925
2932
  }
2926
2933
  const policyCategories = data.policy?.consent?.categories;
2927
- const hasPolicyCategoryAllowlist = Array.isArray(policyCategories) && policyCategories.length > 0 && !policyCategories.includes('*');
2928
- if (hasPolicyCategoryAllowlist) {
2934
+ const hasStrictPolicyCategoryAllowlist = shouldEnforcePolicyCategoryScope(policyCategories, data.policy?.consent?.scopeMode ?? null);
2935
+ if (hasStrictPolicyCategoryAllowlist) {
2929
2936
  const uniqueAllowedCategories = filterConsentCategoriesByPolicy(allConsentNames, policyCategories);
2930
2937
  update.consentCategories = uniqueAllowedCategories;
2931
2938
  update.consents = applyPolicyPurposeAllowlist(update.consents ?? get().consents, uniqueAllowedCategories);
@@ -2934,7 +2941,8 @@ function buildStoreUpdate(data, config, effectiveIABEnabled, initSourceMetadata)
2934
2941
  const preselectedCategories = data.policy?.consent?.preselectedCategories;
2935
2942
  const shouldApplyPreselectedCategories = null === consentInfo && !autoGrantedConsents && Array.isArray(preselectedCategories) && preselectedCategories.length > 0;
2936
2943
  if (shouldApplyPreselectedCategories) {
2937
- const preselectedScope = hasPolicyCategoryAllowlist ? filterConsentCategoriesByPolicy(allConsentNames, policyCategories) : allConsentNames;
2944
+ const displayedConsentNames = update.consentCategories ?? get().consentCategories;
2945
+ const preselectedScope = hasStrictPolicyCategoryAllowlist ? filterConsentCategoriesByPolicy(displayedConsentNames, policyCategories) : displayedConsentNames;
2938
2946
  const allowedPreselectedCategories = filterConsentCategoriesByPolicy(preselectedScope, preselectedCategories);
2939
2947
  const preselectedSet = new Set(allowedPreselectedCategories);
2940
2948
  const selectedConsentBaseline = update.selectedConsents ?? get().selectedConsents;
@@ -3647,8 +3655,14 @@ const createConsentManagerStore = (manager, options = {})=>{
3647
3655
  return resetState;
3648
3656
  });
3649
3657
  },
3650
- setConsentCategories: (types)=>set({
3651
- consentCategories: filterConsentCategoriesByPolicy(types, get().policyCategories)
3658
+ setConsentCategories: (types)=>set(()=>{
3659
+ const { policyCategories, policyScopeMode } = get();
3660
+ if (shouldEnforcePolicyCategoryScope(policyCategories, policyScopeMode)) return {
3661
+ consentCategories: filterConsentCategoriesByPolicy(types, policyCategories)
3662
+ };
3663
+ return {
3664
+ consentCategories: Array.from(new Set(types))
3665
+ };
3652
3666
  }),
3653
3667
  setCallback: (name, callback)=>{
3654
3668
  const currentState = get();
@@ -3720,13 +3734,15 @@ const createConsentManagerStore = (manager, options = {})=>{
3720
3734
  });
3721
3735
  },
3722
3736
  updateConsentCategories: (newCategories)=>{
3737
+ const { consentCategories: currentConsentCategories, policyCategories, policyScopeMode } = get();
3723
3738
  const allCategoriesSet = new Set([
3724
- ...get().consentCategories,
3739
+ ...currentConsentCategories,
3725
3740
  ...newCategories
3726
3741
  ]);
3727
- const allCategories = filterConsentCategoriesByPolicy(Array.from(allCategoriesSet), get().policyCategories);
3742
+ let consentCategories;
3743
+ consentCategories = shouldEnforcePolicyCategoryScope(policyCategories, policyScopeMode) ? filterConsentCategoriesByPolicy(Array.from(allCategoriesSet), policyCategories) : Array.from(allCategoriesSet);
3728
3744
  set({
3729
- consentCategories: allCategories
3745
+ consentCategories
3730
3746
  });
3731
3747
  },
3732
3748
  identifyUser: async (user)=>{
@@ -3754,6 +3770,103 @@ const createConsentManagerStore = (manager, options = {})=>{
3754
3770
  }
3755
3771
  });
3756
3772
  },
3773
+ unstable_acceptPolicyConsent: async (input)=>{
3774
+ const currentState = get();
3775
+ const currentInfo = currentState.consentInfo;
3776
+ const subjectId = currentInfo?.subjectId ?? generateSubjectId();
3777
+ const storedIdentifiers = sanitizeSubjectIdentifiers({
3778
+ externalId: currentInfo?.externalId,
3779
+ identityProvider: currentInfo?.identityProvider
3780
+ });
3781
+ const userIdentifiers = sanitizeSubjectIdentifiers({
3782
+ externalId: currentState.user?.id,
3783
+ identityProvider: currentState.user?.identityProvider
3784
+ });
3785
+ const inputIdentifiers = sanitizeSubjectIdentifiers({
3786
+ externalId: input.externalId,
3787
+ identityProvider: input.identityProvider
3788
+ });
3789
+ const externalId = inputIdentifiers.externalId ?? storedIdentifiers.externalId ?? userIdentifiers.externalId;
3790
+ const identityProvider = inputIdentifiers.identityProvider ?? storedIdentifiers.identityProvider ?? userIdentifiers.identityProvider;
3791
+ const givenAt = input.givenAt ?? Date.now();
3792
+ const domain = input.domain ?? ("u" > typeof window ? window.location.hostname : 'localhost');
3793
+ const isLegalDocumentType = 'privacy_policy' === input.type || 'terms_and_conditions' === input.type || 'dpa' === input.type;
3794
+ let legalDocumentFields = {};
3795
+ if (isLegalDocumentType) if (input.documentSnapshotToken) legalDocumentFields = {
3796
+ documentSnapshotToken: input.documentSnapshotToken
3797
+ };
3798
+ else if (input.policyHash) legalDocumentFields = {
3799
+ policyHash: input.policyHash
3800
+ };
3801
+ else if (input.policyId) legalDocumentFields = {
3802
+ policyId: input.policyId
3803
+ };
3804
+ else throw new Error('Legal document consent requires documentSnapshotToken, policyHash, or policyId.');
3805
+ const response = await manager.setConsent({
3806
+ body: {
3807
+ type: input.type,
3808
+ subjectId,
3809
+ domain,
3810
+ givenAt,
3811
+ uiSource: input.uiSource ?? 'api',
3812
+ ...legalDocumentFields,
3813
+ ...input.metadata ? {
3814
+ metadata: input.metadata
3815
+ } : {},
3816
+ ...input.preferences ? {
3817
+ preferences: input.preferences
3818
+ } : {},
3819
+ ...externalId ? {
3820
+ externalSubjectId: externalId
3821
+ } : {},
3822
+ ...identityProvider ? {
3823
+ identityProvider
3824
+ } : {}
3825
+ }
3826
+ });
3827
+ if (!response.ok || !response.data) {
3828
+ const errorMsg = response.error?.message ?? 'Failed to accept policy consent';
3829
+ get().callbacks.onError?.({
3830
+ error: errorMsg
3831
+ });
3832
+ const error = new Error(errorMsg);
3833
+ error.code = response.error?.code;
3834
+ error.details = response.error?.details ?? null;
3835
+ error.status = response.error?.status;
3836
+ throw error;
3837
+ }
3838
+ const consent = {
3839
+ ...response.data,
3840
+ givenAt: response.data.givenAt instanceof Date ? response.data.givenAt : new Date(response.data.givenAt)
3841
+ };
3842
+ const latestState = get();
3843
+ const latestInfo = latestState.consentInfo;
3844
+ const nextConsentInfo = {
3845
+ ...latestInfo,
3846
+ time: givenAt,
3847
+ subjectId,
3848
+ ...externalId ? {
3849
+ externalId
3850
+ } : {},
3851
+ ...identityProvider ? {
3852
+ identityProvider
3853
+ } : {}
3854
+ };
3855
+ set({
3856
+ consentInfo: nextConsentInfo,
3857
+ ...externalId ? {
3858
+ user: {
3859
+ id: externalId,
3860
+ identityProvider
3861
+ }
3862
+ } : {}
3863
+ });
3864
+ saveConsentToStorage({
3865
+ consents: latestState.consents,
3866
+ consentInfo: nextConsentInfo
3867
+ }, void 0, latestState.storageConfig);
3868
+ return consent;
3869
+ },
3757
3870
  setOverrides: async (overrides)=>{
3758
3871
  set({
3759
3872
  overrides: {
@@ -3883,7 +3996,7 @@ function getOrCreateConsentRuntime(options, pkgInfo) {
3883
3996
  config: {
3884
3997
  ...userConfig ?? {},
3885
3998
  pkg: pkgInfo?.pkg || 'c15t',
3886
- version: pkgInfo?.version || version,
3999
+ version: pkgInfo?.version || "2.0.4",
3887
4000
  mode: normalizedMode,
3888
4001
  meta: {
3889
4002
  ...userConfig?.meta ?? {},
@@ -82,7 +82,7 @@ export type ConsentManagerOptions = {
82
82
  * @remarks
83
83
  * When provided, this takes precedence over `store.offlinePolicy`.
84
84
  *
85
- * @see {@link https://v2.c15t.com/docs/frameworks/javascript/policy-packs}
85
+ * @see {@link https://c15t.com/docs/frameworks/javascript/policy-packs}
86
86
  */
87
87
  offlinePolicy?: StoreOptions['offlinePolicy'];
88
88
  /**
@@ -5,7 +5,7 @@ import type { IABFallbackConfig } from './types';
5
5
  /**
6
6
  * Provides offline mode fallback for showConsentBanner API.
7
7
  * Simulates the behavior of OfflineClient when API requests fail.
8
- * In fallback mode, fetches GVL from gvl.consent.io when IAB is enabled.
8
+ * In fallback mode, fetches GVL from gvl.inth.app when IAB is enabled.
9
9
  * @internal
10
10
  */
11
11
  export declare function offlineFallbackForConsentBanner(options?: FetchOptions<InitResponse>, iabConfig?: IABFallbackConfig): Promise<ResponseContext<InitResponse>>;
@@ -59,7 +59,7 @@ export interface C15tInternalClientOptions {
59
59
  /**
60
60
  * IAB configuration for offline/fallback mode.
61
61
  * When the backend is unavailable and IAB is enabled,
62
- * the client will fetch GVL from gvl.consent.io with these settings.
62
+ * the client will fetch GVL from gvl.inth.app with these settings.
63
63
  */
64
64
  iabConfig?: IABFallbackConfig;
65
65
  }
@@ -6,7 +6,7 @@ import type { IABFallbackConfig } from '../hosted/types';
6
6
  export interface OfflineClientOptions {
7
7
  /**
8
8
  * IAB configuration for offline mode.
9
- * When IAB is enabled, the client will fetch GVL from gvl.consent.io.
9
+ * When IAB is enabled, the client will fetch GVL from gvl.inth.app.
10
10
  */
11
11
  iabConfig?: IABFallbackConfig;
12
12
  /**
@@ -27,7 +27,7 @@ export { buildPrefetchScript } from './libs/prefetch';
27
27
  export { emitScriptDebugEvent, getLoadedScriptIds, isScriptLoaded, loadScripts, type Script, type ScriptDebugAction, type ScriptDebugEvent, type ScriptDebugEventInput, type ScriptDebugListener, type ScriptDebugScope, type ScriptDebugSource, type ScriptLifecycleCallback, subscribeToScriptDebugEvents, unloadScripts, updateScripts, } from './libs/script-loader';
28
28
  export { type ConsentRuntimeOptions, type ConsentRuntimePkgInfo, type ConsentRuntimeResult, clearConsentRuntimeCache, getOrCreateConsentRuntime, } from './runtime';
29
29
  export { createConsentManagerStore } from './store';
30
- export type { ActiveUI, ConsentStoreState, InitDataSource, OfflinePolicyConfig, PolicyScopeMode, PolicySurfaceState, PolicyUiAction, PolicyUiActionDirection, PolicyUiActionGroup, PolicyUiProfile, PolicyUiSurfaceConfig, SSRInitialData, SSRInitRequestContext, SSRInitRequestMetadata, SSRSkippedReason, StoreOptions, } from './store/type';
30
+ export type { ActiveUI, ConsentStoreState, InitDataSource, OfflinePolicyConfig, PolicyScopeMode, PolicySurfaceState, PolicyUiAction, PolicyUiActionDirection, PolicyUiActionGroup, PolicyUiProfile, PolicyUiSurfaceConfig, SSRInitialData, SSRInitRequestContext, SSRInitRequestMetadata, SSRSkippedReason, StoreOptions, UnstableGenericPolicyConsentInput, UnstableLegalDocumentConsentInput, UnstablePolicyConsentInput, } from './store/type';
31
31
  export { defaultTranslationConfig } from './translations';
32
32
  export type { Callback, Callbacks, OnBannerFetchedPayload, OnConsentChangedPayload, OnConsentSetPayload, OnErrorPayload, } from './types/callbacks';
33
33
  export type { ConsentBannerResponse, ConsentState, LocationInfo, NamespaceProps, } from './types/compliance';
@@ -137,7 +137,7 @@ export type IABManager = IABState & IABActions;
137
137
  * @internal
138
138
  */
139
139
  export interface CMPApiConfig {
140
- /** CMP ID registered with IAB Europe. Provided by the backend (consent.io) or client config. */
140
+ /** CMP ID registered with IAB Europe. Provided by the backend (inth.com) or client config. */
141
141
  cmpId?: number;
142
142
  /** CMP version (default: package version from ~/cmp-defaults) */
143
143
  cmpVersion?: number | string;
@@ -216,7 +216,7 @@ export interface IABConfig {
216
216
  * Enable IAB TCF 2.3 mode.
217
217
  *
218
218
  * When enabled, c15t will:
219
- * - Fetch GVL from gvl.consent.io
219
+ * - Fetch GVL from gvl.inth.app
220
220
  * - Initialize __tcfapi CMP API
221
221
  * - Generate TC Strings for IAB compliance
222
222
  *
@@ -236,7 +236,7 @@ export interface IABConfig {
236
236
  /**
237
237
  * CMP ID registered with IAB Europe.
238
238
  *
239
- * When using consent.io as the backend, this is automatically provided
239
+ * When using inth.com as the backend, this is automatically provided
240
240
  * via the `/init` endpoint — no client-side configuration needed.
241
241
  *
242
242
  * Only set this if you self-host and have your own CMP registration.
@@ -325,4 +325,4 @@ export declare const IAB_STORAGE_KEYS: {
325
325
  *
326
326
  * @internal
327
327
  */
328
- export declare const GVL_ENDPOINT = "https://gvl.consent.io";
328
+ export declare const GVL_ENDPOINT = "https://gvl.inth.app";
@@ -20,9 +20,18 @@ export interface PolicyValidationIssue {
20
20
  * Any preference key not in `allowedPurposeIds` is forced to `false`.
21
21
  * This prevents backend allowlist enforcement errors when clients hold
22
22
  * additional consent keys (for example from IAB category mapping).
23
- * When `allowedPurposeIds` contains `*`, no filtering is applied.
23
+ * When `allowedPurposeIds` contains `*`, no filtering is applied. `necessary`
24
+ * is always retained.
24
25
  */
25
26
  export declare function applyPolicyPurposeAllowlist<T extends Record<string, boolean>>(preferences: T, allowedPurposeIds?: string[]): T;
27
+ /**
28
+ * Strips preference keys that are outside the active policy allowlist.
29
+ *
30
+ * Use this for API payloads when the backend enforces strict purpose scope and
31
+ * rejects unknown preference keys entirely. `necessary` is always retained to
32
+ * stay aligned with `applyPolicyPurposeAllowlist()`.
33
+ */
34
+ export declare function stripDisallowedPreferenceKeys<T extends Record<string, boolean>>(preferences: T, allowedPurposeIds?: string[]): Partial<T>;
26
35
  /**
27
36
  * Filters consent categories against a policy purpose allowlist.
28
37
  *
@@ -31,12 +40,15 @@ export declare function applyPolicyPurposeAllowlist<T extends Record<string, boo
31
40
  */
32
41
  export declare function filterConsentCategoriesByPolicy(categories: AllConsentNames[], allowedPurposeIds?: string[] | null): AllConsentNames[];
33
42
  /**
34
- * Applies policy scope to runtime gating behavior.
35
- *
36
- * Out-of-policy categories are treated as permissive by c15t runtime and are
37
- * therefore granted for gating decisions (scripts/iframes load normally).
43
+ * Returns whether policy category scope should be enforced as a hard allowlist.
44
+ */
45
+ export declare function shouldEnforcePolicyCategoryScope(allowedPurposeIds?: string[] | null, scopeMode?: 'strict' | 'permissive' | null): boolean;
46
+ /**
47
+ * @deprecated No-op retained for API compatibility. Runtime gating respects
48
+ * the current consent state directly; policy scope is enforced at category
49
+ * discovery, render, and save time instead.
38
50
  */
39
- export declare function applyPolicyScopeForRuntimeGating(consents: ConsentState, allowedPurposeIds?: string[] | null, scopeMode?: 'strict' | 'permissive' | null): ConsentState;
51
+ export declare function applyPolicyScopeForRuntimeGating(consents: ConsentState, _allowedPurposeIds?: string[] | null, _scopeMode?: 'strict' | 'permissive' | null): ConsentState;
40
52
  /**
41
53
  * Gets the runtime policy returned by /init, if present.
42
54
  */
@@ -15,7 +15,7 @@ export interface PendingConsentSync {
15
15
  subjectId: string;
16
16
  externalId?: string;
17
17
  identityProvider?: string;
18
- preferences: ConsentState;
18
+ preferences: Partial<ConsentState>;
19
19
  givenAt: number;
20
20
  jurisdiction?: string;
21
21
  jurisdictionModel?: string | null;
@@ -2,7 +2,7 @@
2
2
  * @packageDocumentation
3
3
  * Defines the core types and interfaces for the consent management store.
4
4
  */
5
- import type { Branding, InitOutput, PolicyConfig, PolicyScopeMode, PolicyUiAction, PolicyUiActionDirection, PolicyUiActionGroup, PolicyUiProfile, PolicyUiSurfaceConfig } from '@c15t/schema/types';
5
+ import type { Branding, InitOutput, PolicyConfig, PolicyScopeMode, PolicyUiAction, PolicyUiActionDirection, PolicyUiActionGroup, PolicyUiProfile, PolicyUiSurfaceConfig, PostSubjectOutput } from '@c15t/schema/types';
6
6
  import type { Model } from '../libs/determine-model';
7
7
  import type { StorageConfig } from '../libs/cookie';
8
8
  import type { HasCondition } from '../libs/has';
@@ -48,6 +48,53 @@ export interface PolicySurfaceState {
48
48
  /** Scroll lock hint from backend runtime policy. */
49
49
  scrollLock?: boolean;
50
50
  }
51
+ type RequireAtLeastOne<T, Keys extends keyof T = keyof T> = Keys extends keyof T ? Required<Pick<T, Keys>> & Partial<Omit<T, Keys>> : never;
52
+ /**
53
+ * Experimental input for legal-document consent writes.
54
+ *
55
+ * @remarks
56
+ * Preferred identifier flow:
57
+ * - `documentSnapshotToken` for authoritative, signed release metadata
58
+ * - `policyHash` when the caller only knows the rendered document hash
59
+ * - `policyId` only as a compatibility fallback for older backends
60
+ *
61
+ * @experimental
62
+ */
63
+ type UnstableLegalDocumentConsentInputBase = {
64
+ type: 'privacy_policy' | 'terms_and_conditions' | 'dpa';
65
+ policyId?: string;
66
+ policyHash?: string;
67
+ documentSnapshotToken?: string;
68
+ domain?: string;
69
+ givenAt?: number;
70
+ metadata?: Record<string, unknown>;
71
+ preferences?: Record<string, boolean>;
72
+ uiSource?: string;
73
+ externalId?: string;
74
+ identityProvider?: string;
75
+ };
76
+ export type UnstableLegalDocumentConsentInput = UnstableLegalDocumentConsentInputBase & RequireAtLeastOne<Pick<UnstableLegalDocumentConsentInputBase, 'policyId' | 'policyHash' | 'documentSnapshotToken'>>;
77
+ /**
78
+ * Experimental input for non-legal policy consent writes.
79
+ *
80
+ * @experimental
81
+ */
82
+ export interface UnstableGenericPolicyConsentInput {
83
+ type: 'marketing_communications' | 'age_verification' | 'other';
84
+ domain?: string;
85
+ givenAt?: number;
86
+ metadata?: Record<string, unknown>;
87
+ preferences?: Record<string, boolean>;
88
+ uiSource?: string;
89
+ externalId?: string;
90
+ identityProvider?: string;
91
+ }
92
+ /**
93
+ * Experimental input for policy-based consent writes.
94
+ *
95
+ * @experimental
96
+ */
97
+ export type UnstablePolicyConsentInput = UnstableLegalDocumentConsentInput | UnstableGenericPolicyConsentInput;
51
98
  /**
52
99
  * Offline policy preview payload for the headless runtime.
53
100
  *
@@ -61,8 +108,8 @@ export interface PolicySurfaceState {
61
108
  * resolve it locally
62
109
  * - `policy` / `policyDecision`: inject a fully synthetic resolved result
63
110
  *
64
- * @see {@link https://v2.c15t.com/docs/frameworks/javascript/policy-packs}
65
- * @see {@link https://v2.c15t.com/docs/frameworks/react/concepts/policy-packs}
111
+ * @see {@link https://c15t.com/docs/frameworks/javascript/policy-packs}
112
+ * @see {@link https://c15t.com/docs/frameworks/react/concepts/policy-packs}
66
113
  */
67
114
  export type OfflinePolicyConfig = {
68
115
  /**
@@ -198,7 +245,7 @@ export interface SSRInitialData {
198
245
  /**
199
246
  * Global Vendor List data for IAB TCF mode.
200
247
  * - `undefined` means IAB is not active for the request or not enabled on server
201
- * - `null` means the user is in a non-IAB region (204 response from gvl.consent.io)
248
+ * - `null` means the user is in a non-IAB region (204 response from gvl.inth.app)
202
249
  * - `GlobalVendorList` contains the vendor list data from init response
203
250
  *
204
251
  * Note: When init returns 200 without gvl, client IAB settings are overridden to disabled.
@@ -422,7 +469,7 @@ export interface StoreOptions extends Partial<StoreConfig> {
422
469
  * Note: If the server returns 200 without GVL, client IAB settings are
423
470
  * automatically overridden to disabled (server takes precedence).
424
471
  *
425
- * In offline/fallback mode, GVL is fetched from gvl.consent.io.
472
+ * In offline/fallback mode, GVL is fetched from gvl.inth.app.
426
473
  *
427
474
  * This is an opt-in feature with zero bundle impact when not enabled.
428
475
  *
@@ -440,7 +487,7 @@ export interface StoreOptions extends Partial<StoreConfig> {
440
487
  *
441
488
  * Ignored in hosted/custom modes.
442
489
  *
443
- * @see {@link https://v2.c15t.com/docs/frameworks/javascript/policy-packs}
490
+ * @see {@link https://c15t.com/docs/frameworks/javascript/policy-packs}
444
491
  */
445
492
  offlinePolicy?: OfflinePolicyConfig;
446
493
  /**
@@ -646,6 +693,12 @@ export interface StoreActions {
646
693
  * @throws {Error} When the underlying identify-user request fails
647
694
  */
648
695
  identifyUser: (user: User) => Promise<void>;
696
+ /**
697
+ * Writes a policy-based consent such as terms and conditions.
698
+ *
699
+ * @experimental
700
+ */
701
+ unstable_acceptPolicyConsent: (input: UnstablePolicyConsentInput) => Promise<PostSubjectOutput>;
649
702
  /**
650
703
  * Updates the selected consent state for a specific consent type.
651
704
  *
@@ -1 +1 @@
1
- export declare const version = "2.0.0-rc.8";
1
+ export declare const version = "2.0.4";
@@ -0,0 +1,111 @@
1
+ ---
2
+ title: AI Agents
3
+ description: Integrate c15t with AI coding assistants using the docs bundled in each package and c15t agent skills. Give agents version-matched local docs for consent management, banners, script loading, callbacks, and integrations.
4
+ lastModified: 2026-03-24
5
+ ---
6
+ ## Bundled Docs
7
+
8
+ Every supported c15t package now ships docs inside the installed package itself.
9
+
10
+ ### Where to find them
11
+
12
+ * `node_modules/c15t/docs/README.md`
13
+ * `node_modules/@c15t/react/docs/README.md`
14
+ * `node_modules/@c15t/nextjs/docs/README.md`
15
+ * `node_modules/@c15t/backend/docs/README.md`
16
+
17
+ Start with the package `README.md`, then follow its linked pages for the relevant workflow.
18
+
19
+ These docs are version-matched to the exact c15t package version in your project, including generated reference content like prop and type tables.
20
+
21
+ ### Why use them
22
+
23
+ If your app uses multiple c15t packages, use the docs from each relevant installed package instead of relying on stale model knowledge.
24
+
25
+ ### Agent philosophy
26
+
27
+ When an AI tool is helping with c15t behavior, it should read the installed c15t docs first and use model knowledge second. That keeps consent flows, script gating, banner behavior, and integrations aligned with the exact version you have installed.
28
+
29
+ ### Customization ladder for agents
30
+
31
+ When an agent is working on consent UI, it should choose the lowest-power tool that solves the task:
32
+
33
+ 1. Start with the pre-built component and its existing props or provider options
34
+ 2. Use `theme` tokens for semantic visual changes
35
+ 3. Use `theme.slots` for targeted styling of specific parts
36
+ 4. Use CSS variables or className-level overrides only when integrating with external styles
37
+ 5. Use compound components only when the markup order must change
38
+ 6. Use `noStyle` only when c15t structure is still correct but all styling must be replaced
39
+ 7. Use headless hooks only when markup and behavior both need to be rebuilt
40
+
41
+ For common tasks:
42
+
43
+ * Banner footer background -> `theme.colors.surfaceHover`
44
+ * Banner card background -> `theme.colors.surface`
45
+ * Banner card/footer/title tweaks -> banner slots
46
+ * Stock action styling -> `theme.consentActions`
47
+ * Copy changes -> `ConsentManagerProvider.options.i18n`
48
+
49
+ If a token appears not to work, the agent should verify the token-to-component mapping before suggesting CSS overrides, `!important`, `noStyle`, or headless mode.
50
+
51
+ ***
52
+
53
+ ## Agent Skills
54
+
55
+ c15t publishes agent skills that give AI coding assistants deep knowledge of c15t's APIs, components, and configuration. Skills are reusable workflows and tool-specific guidance, not version-matched local docs.
56
+
57
+ ### Installation
58
+
59
+ Via the c15t CLI:
60
+
61
+ |Package manager|Command|
62
+ |:--|:--|
63
+ |npm|`npx @c15t/cli install-skills`|
64
+ |pnpm|`pnpm dlx @c15t/cli install-skills`|
65
+ |yarn|`yarn dlx @c15t/cli install-skills`|
66
+ |bun|`bunx @c15t/cli install-skills`|
67
+
68
+ Or directly:
69
+
70
+ |Package manager|Command|
71
+ |:--|:--|
72
+ |npm|`npx skills add c15t/skills`|
73
+ |pnpm|`pnpm dlx skills add c15t/skills`|
74
+ |yarn|`yarn dlx skills add c15t/skills`|
75
+ |bun|`bunx skills add c15t/skills`|
76
+
77
+ ### What skills provide
78
+
79
+ * **Styling customization** — strict escalation guidance across props, tokens, slots, CSS variables, compound components, `noStyle`, and headless
80
+ * **Internationalization** — translation setup, locale routing integration
81
+ * **Script management** — configuring third-party scripts with consent categories
82
+ * **Component setup** — ConsentBanner, ConsentDialog, provider configuration
83
+
84
+ ### Supported tools
85
+
86
+ * Claude Code
87
+ * Cursor
88
+ * GitHub Copilot (via `.github/skills`)
89
+ * Any agent that supports the skills format
90
+
91
+ ***
92
+
93
+ ## When to use which
94
+
95
+ Use bundled docs when:
96
+
97
+ * Your agent can read files in the local project
98
+ * You want version-matched docs from the installed c15t packages
99
+ * You want a package-local README that tells the agent which detailed docs to read first
100
+ * You want concrete guidance for consent management, cookie banners, consent dialogs, preference centers, script loading, callbacks, and integrations
101
+
102
+ Use agent skills when:
103
+
104
+ * Your tool supports the skills ecosystem
105
+ * You want reusable workflows and tool-specific guidance that can point back to the installed package README files
106
+
107
+ Use both when:
108
+
109
+ * Your tool supports both local file context and skills
110
+ * You want local package docs plus reusable setup and configuration help
111
+ * You want the bundled package docs as the source of truth plus a reusable decision tree for customization
@@ -15,7 +15,7 @@ c15t supports three client modes that determine how consent data is stored and s
15
15
 
16
16
  ## Hosted Mode (Recommended)
17
17
 
18
- The default mode. Connects to a c15t backend for full consent lifecycle management. We recommend using [consent.io](https://consent.io) for a fully managed experience, but you can [self-host](/docs/self-host) as well.
18
+ The default mode. Connects to a c15t backend for full consent lifecycle management. We recommend using [inth.com](https://inth.com) for a fully managed experience, but you can [self-host](/docs/self-host) as well.
19
19
 
20
20
  > ℹ️ **Info:**
21
21
  > mode: 'hosted' is the preferred value. The legacy alias mode: 'c15t' is still supported for backward compatibility.
@@ -8,7 +8,7 @@ A policy pack is an ordered array of policies. Each policy targets a region or c
8
8
 
9
9
  There are three ways to configure policy packs:
10
10
 
11
- 1. **consent.io (recommended)** — use [consent.io](https://consent.io) as your hosted backend. Configure packs visually in the dashboard or via API — no code changes required. Works with any frontend, including static sites.
11
+ 1. **inth.com (recommended)** — use [inth.com](https://inth.com) as your hosted backend. Configure packs visually in the dashboard or via API — no code changes required. Works with any frontend, including static sites.
12
12
  2. **Self-hosted backend** — define packs in code via `policyPacks` and resolve them from real request geo data. Full control over policy logic and storage.
13
13
  3. **Offline fallback** — pass the same policy shapes to the frontend via `offlinePolicy.policyPacks`. Use this mainly for local development, demos, deterministic testing, or resilience when the backend is temporarily unreachable. If you omit `offlinePolicy.policyPacks`, c15t falls back to a synthetic worldwide opt-in banner instead of no-banner mode.
14
14
 
@@ -15,12 +15,12 @@ When your site participates in the IAB ecosystem (ad exchanges, SSPs, DSPs, DMPs
15
15
 
16
16
  ## CMP Registration
17
17
 
18
- [consent.io](https://consent.io) is pending validation as an IAB Europe-registered CMP for c15t. Once approved, when you use consent.io as your backend, the correct CMP ID will be automatically provided to your client via the `/init` endpoint — no client-side configuration needed.
18
+ [Inth](https://inth.com), c15t's hosted platform, is IAB TCF certified. When you use Inth as your backend with c15t's prebuilt IAB UI, the correct CMP ID is automatically provided to your client via the `/init` endpoint — no client-side configuration needed.
19
19
 
20
- If you self-host the c15t backend and have your own CMP registration with IAB Europe, you can configure your CMP ID on the backend via `advanced.iab.cmpId` or on the client via the `iab.cmpId` option. A valid (non-zero) CMP ID is required for IAB TCF compliance.
20
+ If you self-host the c15t backend or want to operate as your own CMP, register your own CMP with IAB Europe and configure your CMP ID on the backend via `advanced.iab.cmpId` or on the client via the `iab.cmpId` option. Registering your own CMP may also involve IAB Europe fees, so check IAB Europe's current CMP registration terms and pricing before choosing this route. A valid (non-zero) CMP ID is required for IAB TCF compliance.
21
21
 
22
22
  > ℹ️ **Info:**
23
- > If you heavily customize or build your own IAB banner or dialog (rather than using the default IABConsentBanner and IABConsentDialog components), you cannot use consent.io's CMP ID. You must register your own CMP with IAB Europe and use your own CMP ID.
23
+ > If you heavily customize or build your own IAB banner or dialog instead of using the default IABConsentBanner and IABConsentDialog components, you cannot use Inth's CMP ID. You must register your own CMP with IAB Europe and use your own CMP ID.
24
24
 
25
25
  ## How c15t Implements TCF
26
26
 
@@ -40,17 +40,17 @@ Enable IAB TCF in the runtime options:
40
40
 
41
41
  ```ts
42
42
  import { getOrCreateConsentRuntime } from 'c15t';
43
+ import { iab } from '@c15t/iab';
43
44
 
44
45
  const { consentStore } = getOrCreateConsentRuntime({
45
46
  mode: 'hosted',
46
47
  backendURL: 'https://your-instance.c15t.dev',
47
- iab: {
48
- enabled: true,
48
+ iab: iab({
49
49
  vendors: [1, 2, 10, 25], // IAB vendor IDs you work with
50
- // cmpId is automatically provided by the backend (consent.io).
50
+ // cmpId is automatically provided by the backend (inth.com).
51
51
  // Only set this if you have your own CMP registration with IAB Europe.
52
52
  // cmpId: 123,
53
- },
53
+ }),
54
54
  });
55
55
  ```
56
56
 
@@ -91,14 +91,13 @@ window.__tcfapi('getTCData', 2, (tcData, success) => {
91
91
 
92
92
  ## IAB Configuration Options
93
93
 
94
- The `iab` option on the provider accepts:
94
+ Configure IAB mode with `iab({ ... })` from `@c15t/iab`. The factory enables the addon and injects the runtime module automatically. The user-facing options are:
95
95
 
96
96
  |Option|Type|Description|
97
97
  |--|--|--|
98
- |`enabled`|`boolean`|Enable IAB TCF mode|
99
- |`cmpId`|`number`|CMP ID registered with IAB Europe. Automatically provided by the backend when using consent.io. Only set this if you have your own CMP registration.|
98
+ |`cmpId`|`number`|CMP ID registered with IAB Europe. Automatically provided by the backend when using Inth with the prebuilt IAB UI. Only set this if you have your own CMP registration.|
100
99
  |`vendors`|`number[]`|IAB vendor IDs that your site works with|
101
- |`nonIABVendors`|`NonIABVendor[]`|Custom vendors not in the IAB registry|
100
+ |`customVendors`|`NonIABVendor[]`|Custom vendors not in the IAB registry|
102
101
 
103
102
  ## Key Concepts
104
103
 
@@ -10,7 +10,7 @@ There are two ways c15t can load translations: client-side or server-side.
10
10
 
11
11
  |Server-side|Client-side|
12
12
  |--|--|
13
- |The best way to reduce bundle size and improve performance. We can detect the user's language based on the browser's language settings, allowing for the most accurate translations. By default, when using a [consent.io](https://consent.io) hosted instance, [these languages](https://github.com/c15t/c15t/tree/main/packages/translations/src/translations) are supported.|Bundled with the application allowing for multiple languages to be supported without the need for a backend. The more translations you have, the larger the bundle size will be, which may impact the performance of your application.|
13
+ |The best way to reduce bundle size and improve performance. We can detect the user's language based on the browser's language settings, allowing for the most accurate translations. By default, when using a [inth.com](https://inth.com) hosted instance, [these languages](https://github.com/c15t/c15t/tree/main/packages/translations/src/translations) are supported.|Bundled with the application allowing for multiple languages to be supported without the need for a backend. The more translations you have, the larger the bundle size will be, which may impact the performance of your application.|
14
14
 
15
15
  ## Basic Configuration
16
16
 
@@ -126,10 +126,10 @@ Install c15t agent skills to let AI agents help with styling, i18n, scripts & ot
126
126
 
127
127
  |Package manager|Command|
128
128
  |:--|:--|
129
- |npm|`npx @c15t/cli@rc skills`|
130
- |pnpm|`pnpm dlx @c15t/cli@rc skills`|
131
- |yarn|`yarn dlx @c15t/cli@rc skills`|
132
- |bun|`bunx @c15t/cli@rc skills`|
129
+ |npm|`npx @c15t/cli skills`|
130
+ |pnpm|`pnpm dlx @c15t/cli skills`|
131
+ |yarn|`yarn dlx @c15t/cli skills`|
132
+ |bun|`bunx @c15t/cli skills`|
133
133
 
134
134
  See [AI Agents](/docs/ai-agents) for bundled package docs and agent skills.
135
135
 
package/package.json CHANGED
@@ -1,32 +1,42 @@
1
1
  {
2
2
  "name": "c15t",
3
- "version": "2.0.0-rc.8",
4
- "description": "Developer-first CMP for JavaScript: cookie banner, consent manager, preferences centre. GDPR ready with minimal setup and rich customization",
3
+ "version": "2.0.4",
4
+ "description": "Headless cookie banner, consent manager & preference center for JavaScript / TypeScript. GDPR, CCPA, LGPD and IAB TCF compliant.",
5
5
  "keywords": [
6
- "nextjs",
7
6
  "consent",
8
7
  "privacy",
9
8
  "gdpr",
10
9
  "ccpa",
11
10
  "lgpd",
12
- "server-side rendering",
13
- "react",
14
- "typescript",
15
- "cookie-banner",
16
- "consent-management-platform",
11
+ "tcf",
12
+ "iab",
17
13
  "cmp",
14
+ "consent-management",
15
+ "consent-management-platform",
16
+ "cookie-banner",
17
+ "cookie-consent",
18
+ "cookies",
18
19
  "consent-banner",
19
- "user-consent",
20
- "privacy-compliance",
21
- "web-privacy"
20
+ "consent-manager",
21
+ "preference-center",
22
+ "headless",
23
+ "javascript",
24
+ "typescript",
25
+ "ssr",
26
+ "server-side-rendering",
27
+ "tracking-consent",
28
+ "eu-cookie-law"
22
29
  ],
23
30
  "homepage": "https://c15t.com/docs/frameworks/javascript/quickstart",
31
+ "bugs": {
32
+ "url": "https://github.com/c15t/c15t/issues"
33
+ },
24
34
  "repository": {
25
35
  "type": "git",
26
36
  "url": "https://github.com/c15t/c15t.git",
27
37
  "directory": "packages/core"
28
38
  },
29
- "license": "GPL-3.0-only",
39
+ "license": "Apache-2.0",
30
40
  "type": "module",
31
41
  "exports": {
32
42
  ".": {
@@ -51,10 +61,10 @@
51
61
  "build:agent-docs": "bun ../../scripts/agent-docs/generate-package-docs.ts c15t",
52
62
  "check-types": "bun prebuild && tsc --noEmit",
53
63
  "check-types:test": "tsc -p tsconfig.test.json",
54
- "dev": "bun prebuild && rslib build && bun ../../scripts/normalize-dist-types.mjs",
64
+ "dev": "sh -c 'bun prebuild && rslib build --no-dts --no-clean && rslib build --watch --no-dts --no-clean'",
55
65
  "fmt": "bun biome format --write . && bun biome check --formatter-enabled=false --linter-enabled=false --write",
56
66
  "lint": "bun biome lint ./src",
57
- "prepack": "cd ../.. && bunx turbo run build --filter=c15t",
67
+ "prepack": "bun ../../scripts/verify-package-artifacts.ts",
58
68
  "test": "bun prebuild && vitest run",
59
69
  "test:watch": "bun prebuild && vitest"
60
70
  },
@@ -64,12 +74,12 @@
64
74
  "not op_mini all"
65
75
  ],
66
76
  "dependencies": {
67
- "@c15t/schema": "2.0.0-rc.5",
68
- "@c15t/translations": "2.0.0-rc.8",
77
+ "@c15t/schema": "2.0.1",
78
+ "@c15t/translations": "2.0.0",
69
79
  "zustand": "5.0.12"
70
80
  },
71
81
  "devDependencies": {
72
- "@c15t/typescript-config": "0.0.1-beta.1",
82
+ "@c15t/typescript-config": "0.0.1",
73
83
  "@c15t/vitest-config": "1.0.0",
74
84
  "genversion": "3.2.0",
75
85
  "vitest-localstorage-mock": "0.1.2"
package/readme.json CHANGED
@@ -12,7 +12,7 @@
12
12
  "JavaScript or TypeScript project",
13
13
  "Node.js 18.17.0 or later",
14
14
  "npm, pnpm, or yarn package manager",
15
- "A hosted [c15t instance](https://consent.io) (free sign-up) or [self-hosted deployment](https://c15t.com/docs/self-host/v2)"
15
+ "A hosted [c15t instance](https://inth.com) (free sign-up) or [self-hosted deployment](https://c15t.com/docs/self-host/v2)"
16
16
  ],
17
17
  "manualInstallation": [
18
18
  "",