c15t 2.0.0-rc.8 → 2.0.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/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # c15t
2
2
 
3
+ ## 2.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - 32617c9: Changelog available at https://c15t.com/changelog/2026-04-14-v2.0.0
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [32617c9]
12
+ - Updated dependencies [32617c9]
13
+ - @c15t/schema@2.0.0
14
+ - @c15t/translations@2.0.0
15
+
16
+ ## 2.0.0-rc.10
17
+
18
+ ### Patch Changes
19
+
20
+ - 9579b62: Add token-first legal-document consent groundwork for `2.0`.
21
+
22
+ - `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.
23
+ - `@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.
24
+ - `@c15t/schema`: extend the subject consent schema and error shapes for legal-document snapshot tokens and hash-based legal-document resolution.
25
+
26
+ - Updated dependencies [9579b62]
27
+ - @c15t/schema@2.0.0-rc.6
28
+
3
29
  ## 2.0.0-rc.8
4
30
 
5
31
  ### Minor Changes
@@ -171,7 +197,7 @@
171
197
 
172
198
  ### Major Changes
173
199
 
174
- - 126a78b: https://v2.c15t.com/changelog/2026-02-12-v2.0.0-rc.0
200
+ - 126a78b: https://c15t.com/changelog/2026-02-12-v2.0.0-rc.0
175
201
 
176
202
  ### Patch Changes
177
203
 
package/README.md CHANGED
@@ -11,7 +11,7 @@
11
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)
@@ -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=github&utm_medium=repopage_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.0",
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;
@@ -2801,6 +2813,7 @@ async function saveConsents({ manager, type, get, set, options, emitConsentChang
2801
2813
  } else if ('necessary' === type) for (const consent of consentTypes)newConsents[consent.name] = true === consent.disabled ? consent.defaultValue : false;
2802
2814
  const policyCategories = getEffectivePolicy(lastBannerFetchData)?.consent?.categories;
2803
2815
  const effectiveConsents = applyPolicyPurposeAllowlist(newConsents, policyCategories);
2816
+ const requestPreferences = stripDisallowedPreferenceKeys(effectiveConsents, policyCategories);
2804
2817
  const didChange = haveConsentsChanged(previousConsents, effectiveConsents, consentTypes);
2805
2818
  const nextConsentCategoryLists = getConsentCategoryLists(effectiveConsents, consentCategories, consentTypes);
2806
2819
  const previousConsentCategoryLists = getConsentCategoryLists(previousConsents, consentCategories, consentTypes);
@@ -2851,7 +2864,7 @@ async function saveConsents({ manager, type, get, set, options, emitConsentChang
2851
2864
  const pendingSync = {
2852
2865
  type,
2853
2866
  subjectId,
2854
- preferences: effectiveConsents,
2867
+ preferences: requestPreferences,
2855
2868
  givenAt,
2856
2869
  jurisdiction: locationInfo?.jurisdiction ?? void 0,
2857
2870
  jurisdictionModel: model,
@@ -2890,7 +2903,7 @@ async function saveConsents({ manager, type, get, set, options, emitConsentChang
2890
2903
  body: {
2891
2904
  type: 'cookie_banner',
2892
2905
  domain: window.location.hostname,
2893
- preferences: effectiveConsents,
2906
+ preferences: requestPreferences,
2894
2907
  subjectId,
2895
2908
  jurisdiction: locationInfo?.jurisdiction ?? void 0,
2896
2909
  jurisdictionModel: model ?? void 0,
@@ -3831,6 +3844,103 @@ const createConsentManagerStore = (manager, options = {})=>{
3831
3844
  }
3832
3845
  });
3833
3846
  },
3847
+ unstable_acceptPolicyConsent: async (input)=>{
3848
+ const currentState = get();
3849
+ const currentInfo = currentState.consentInfo;
3850
+ const subjectId = currentInfo?.subjectId ?? generateSubjectId();
3851
+ const storedIdentifiers = sanitizeSubjectIdentifiers({
3852
+ externalId: currentInfo?.externalId,
3853
+ identityProvider: currentInfo?.identityProvider
3854
+ });
3855
+ const userIdentifiers = sanitizeSubjectIdentifiers({
3856
+ externalId: currentState.user?.id,
3857
+ identityProvider: currentState.user?.identityProvider
3858
+ });
3859
+ const inputIdentifiers = sanitizeSubjectIdentifiers({
3860
+ externalId: input.externalId,
3861
+ identityProvider: input.identityProvider
3862
+ });
3863
+ const externalId = inputIdentifiers.externalId ?? storedIdentifiers.externalId ?? userIdentifiers.externalId;
3864
+ const identityProvider = inputIdentifiers.identityProvider ?? storedIdentifiers.identityProvider ?? userIdentifiers.identityProvider;
3865
+ const givenAt = input.givenAt ?? Date.now();
3866
+ const domain = input.domain ?? ("u" > typeof window ? window.location.hostname : 'localhost');
3867
+ const isLegalDocumentType = 'privacy_policy' === input.type || 'terms_and_conditions' === input.type || 'dpa' === input.type;
3868
+ let legalDocumentFields = {};
3869
+ if (isLegalDocumentType) if (input.documentSnapshotToken) legalDocumentFields = {
3870
+ documentSnapshotToken: input.documentSnapshotToken
3871
+ };
3872
+ else if (input.policyHash) legalDocumentFields = {
3873
+ policyHash: input.policyHash
3874
+ };
3875
+ else if (input.policyId) legalDocumentFields = {
3876
+ policyId: input.policyId
3877
+ };
3878
+ else throw new Error('Legal document consent requires documentSnapshotToken, policyHash, or policyId.');
3879
+ const response = await manager.setConsent({
3880
+ body: {
3881
+ type: input.type,
3882
+ subjectId,
3883
+ domain,
3884
+ givenAt,
3885
+ uiSource: input.uiSource ?? 'api',
3886
+ ...legalDocumentFields,
3887
+ ...input.metadata ? {
3888
+ metadata: input.metadata
3889
+ } : {},
3890
+ ...input.preferences ? {
3891
+ preferences: input.preferences
3892
+ } : {},
3893
+ ...externalId ? {
3894
+ externalSubjectId: externalId
3895
+ } : {},
3896
+ ...identityProvider ? {
3897
+ identityProvider
3898
+ } : {}
3899
+ }
3900
+ });
3901
+ if (!response.ok || !response.data) {
3902
+ const errorMsg = response.error?.message ?? 'Failed to accept policy consent';
3903
+ get().callbacks.onError?.({
3904
+ error: errorMsg
3905
+ });
3906
+ const error = new Error(errorMsg);
3907
+ error.code = response.error?.code;
3908
+ error.details = response.error?.details ?? null;
3909
+ error.status = response.error?.status;
3910
+ throw error;
3911
+ }
3912
+ const consent = {
3913
+ ...response.data,
3914
+ givenAt: response.data.givenAt instanceof Date ? response.data.givenAt : new Date(response.data.givenAt)
3915
+ };
3916
+ const latestState = get();
3917
+ const latestInfo = latestState.consentInfo;
3918
+ const nextConsentInfo = {
3919
+ ...latestInfo,
3920
+ time: givenAt,
3921
+ subjectId,
3922
+ ...externalId ? {
3923
+ externalId
3924
+ } : {},
3925
+ ...identityProvider ? {
3926
+ identityProvider
3927
+ } : {}
3928
+ };
3929
+ set({
3930
+ consentInfo: nextConsentInfo,
3931
+ ...externalId ? {
3932
+ user: {
3933
+ id: externalId,
3934
+ identityProvider
3935
+ }
3936
+ } : {}
3937
+ });
3938
+ saveConsentToStorage({
3939
+ consents: latestState.consents,
3940
+ consentInfo: nextConsentInfo
3941
+ }, void 0, latestState.storageConfig);
3942
+ return consent;
3943
+ },
3834
3944
  setOverrides: async (overrides)=>{
3835
3945
  set({
3836
3946
  overrides: {
@@ -3960,7 +4070,7 @@ function getOrCreateConsentRuntime(options, pkgInfo) {
3960
4070
  config: {
3961
4071
  ...userConfig ?? {},
3962
4072
  pkg: pkgInfo?.pkg || 'c15t',
3963
- version: pkgInfo?.version || version,
4073
+ version: pkgInfo?.version || "2.0.0",
3964
4074
  mode: normalizedMode,
3965
4075
  meta: {
3966
4076
  ...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.0",
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;
@@ -2724,6 +2736,7 @@ async function saveConsents({ manager, type, get, set, options, emitConsentChang
2724
2736
  } else if ('necessary' === type) for (const consent of consentTypes)newConsents[consent.name] = true === consent.disabled ? consent.defaultValue : false;
2725
2737
  const policyCategories = getEffectivePolicy(lastBannerFetchData)?.consent?.categories;
2726
2738
  const effectiveConsents = applyPolicyPurposeAllowlist(newConsents, policyCategories);
2739
+ const requestPreferences = stripDisallowedPreferenceKeys(effectiveConsents, policyCategories);
2727
2740
  const didChange = haveConsentsChanged(previousConsents, effectiveConsents, consentTypes);
2728
2741
  const nextConsentCategoryLists = getConsentCategoryLists(effectiveConsents, consentCategories, consentTypes);
2729
2742
  const previousConsentCategoryLists = getConsentCategoryLists(previousConsents, consentCategories, consentTypes);
@@ -2774,7 +2787,7 @@ async function saveConsents({ manager, type, get, set, options, emitConsentChang
2774
2787
  const pendingSync = {
2775
2788
  type,
2776
2789
  subjectId,
2777
- preferences: effectiveConsents,
2790
+ preferences: requestPreferences,
2778
2791
  givenAt,
2779
2792
  jurisdiction: locationInfo?.jurisdiction ?? void 0,
2780
2793
  jurisdictionModel: model,
@@ -2813,7 +2826,7 @@ async function saveConsents({ manager, type, get, set, options, emitConsentChang
2813
2826
  body: {
2814
2827
  type: 'cookie_banner',
2815
2828
  domain: window.location.hostname,
2816
- preferences: effectiveConsents,
2829
+ preferences: requestPreferences,
2817
2830
  subjectId,
2818
2831
  jurisdiction: locationInfo?.jurisdiction ?? void 0,
2819
2832
  jurisdictionModel: model ?? void 0,
@@ -3754,6 +3767,103 @@ const createConsentManagerStore = (manager, options = {})=>{
3754
3767
  }
3755
3768
  });
3756
3769
  },
3770
+ unstable_acceptPolicyConsent: async (input)=>{
3771
+ const currentState = get();
3772
+ const currentInfo = currentState.consentInfo;
3773
+ const subjectId = currentInfo?.subjectId ?? generateSubjectId();
3774
+ const storedIdentifiers = sanitizeSubjectIdentifiers({
3775
+ externalId: currentInfo?.externalId,
3776
+ identityProvider: currentInfo?.identityProvider
3777
+ });
3778
+ const userIdentifiers = sanitizeSubjectIdentifiers({
3779
+ externalId: currentState.user?.id,
3780
+ identityProvider: currentState.user?.identityProvider
3781
+ });
3782
+ const inputIdentifiers = sanitizeSubjectIdentifiers({
3783
+ externalId: input.externalId,
3784
+ identityProvider: input.identityProvider
3785
+ });
3786
+ const externalId = inputIdentifiers.externalId ?? storedIdentifiers.externalId ?? userIdentifiers.externalId;
3787
+ const identityProvider = inputIdentifiers.identityProvider ?? storedIdentifiers.identityProvider ?? userIdentifiers.identityProvider;
3788
+ const givenAt = input.givenAt ?? Date.now();
3789
+ const domain = input.domain ?? ("u" > typeof window ? window.location.hostname : 'localhost');
3790
+ const isLegalDocumentType = 'privacy_policy' === input.type || 'terms_and_conditions' === input.type || 'dpa' === input.type;
3791
+ let legalDocumentFields = {};
3792
+ if (isLegalDocumentType) if (input.documentSnapshotToken) legalDocumentFields = {
3793
+ documentSnapshotToken: input.documentSnapshotToken
3794
+ };
3795
+ else if (input.policyHash) legalDocumentFields = {
3796
+ policyHash: input.policyHash
3797
+ };
3798
+ else if (input.policyId) legalDocumentFields = {
3799
+ policyId: input.policyId
3800
+ };
3801
+ else throw new Error('Legal document consent requires documentSnapshotToken, policyHash, or policyId.');
3802
+ const response = await manager.setConsent({
3803
+ body: {
3804
+ type: input.type,
3805
+ subjectId,
3806
+ domain,
3807
+ givenAt,
3808
+ uiSource: input.uiSource ?? 'api',
3809
+ ...legalDocumentFields,
3810
+ ...input.metadata ? {
3811
+ metadata: input.metadata
3812
+ } : {},
3813
+ ...input.preferences ? {
3814
+ preferences: input.preferences
3815
+ } : {},
3816
+ ...externalId ? {
3817
+ externalSubjectId: externalId
3818
+ } : {},
3819
+ ...identityProvider ? {
3820
+ identityProvider
3821
+ } : {}
3822
+ }
3823
+ });
3824
+ if (!response.ok || !response.data) {
3825
+ const errorMsg = response.error?.message ?? 'Failed to accept policy consent';
3826
+ get().callbacks.onError?.({
3827
+ error: errorMsg
3828
+ });
3829
+ const error = new Error(errorMsg);
3830
+ error.code = response.error?.code;
3831
+ error.details = response.error?.details ?? null;
3832
+ error.status = response.error?.status;
3833
+ throw error;
3834
+ }
3835
+ const consent = {
3836
+ ...response.data,
3837
+ givenAt: response.data.givenAt instanceof Date ? response.data.givenAt : new Date(response.data.givenAt)
3838
+ };
3839
+ const latestState = get();
3840
+ const latestInfo = latestState.consentInfo;
3841
+ const nextConsentInfo = {
3842
+ ...latestInfo,
3843
+ time: givenAt,
3844
+ subjectId,
3845
+ ...externalId ? {
3846
+ externalId
3847
+ } : {},
3848
+ ...identityProvider ? {
3849
+ identityProvider
3850
+ } : {}
3851
+ };
3852
+ set({
3853
+ consentInfo: nextConsentInfo,
3854
+ ...externalId ? {
3855
+ user: {
3856
+ id: externalId,
3857
+ identityProvider
3858
+ }
3859
+ } : {}
3860
+ });
3861
+ saveConsentToStorage({
3862
+ consents: latestState.consents,
3863
+ consentInfo: nextConsentInfo
3864
+ }, void 0, latestState.storageConfig);
3865
+ return consent;
3866
+ },
3757
3867
  setOverrides: async (overrides)=>{
3758
3868
  set({
3759
3869
  overrides: {
@@ -3883,7 +3993,7 @@ function getOrCreateConsentRuntime(options, pkgInfo) {
3883
3993
  config: {
3884
3994
  ...userConfig ?? {},
3885
3995
  pkg: pkgInfo?.pkg || 'c15t',
3886
- version: pkgInfo?.version || version,
3996
+ version: pkgInfo?.version || "2.0.0",
3887
3997
  mode: normalizedMode,
3888
3998
  meta: {
3889
3999
  ...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
  /**
@@ -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';
@@ -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
  *
@@ -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
  /**
@@ -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.0";
@@ -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.com](https://inth.com) is pending validation as an IAB Europe-registered CMP for c15t. Once approved, when you use inth.com as your backend, the correct CMP ID will be automatically provided to your client via the `/init` endpoint — no client-side configuration needed.
19
19
 
20
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.
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 (rather than using the default IABConsentBanner and IABConsentDialog components), you cannot use inth.com'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.com. 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,6 +1,6 @@
1
1
  {
2
2
  "name": "c15t",
3
- "version": "2.0.0-rc.8",
3
+ "version": "2.0.0",
4
4
  "description": "Developer-first CMP for JavaScript: cookie banner, consent manager, preferences centre. GDPR ready with minimal setup and rich customization",
5
5
  "keywords": [
6
6
  "nextjs",
@@ -26,7 +26,7 @@
26
26
  "url": "https://github.com/c15t/c15t.git",
27
27
  "directory": "packages/core"
28
28
  },
29
- "license": "GPL-3.0-only",
29
+ "license": "Apache-2.0",
30
30
  "type": "module",
31
31
  "exports": {
32
32
  ".": {
@@ -64,12 +64,12 @@
64
64
  "not op_mini all"
65
65
  ],
66
66
  "dependencies": {
67
- "@c15t/schema": "2.0.0-rc.5",
68
- "@c15t/translations": "2.0.0-rc.8",
67
+ "@c15t/schema": "2.0.0",
68
+ "@c15t/translations": "2.0.0",
69
69
  "zustand": "5.0.12"
70
70
  },
71
71
  "devDependencies": {
72
- "@c15t/typescript-config": "0.0.1-beta.1",
72
+ "@c15t/typescript-config": "0.0.1",
73
73
  "@c15t/vitest-config": "1.0.0",
74
74
  "genversion": "3.2.0",
75
75
  "vitest-localstorage-mock": "0.1.2"