@consentify/core 0.1.0 → 1.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/README.md +90 -16
- package/dist/index.d.ts +2 -0
- package/dist/index.js +39 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ npm install @consentify/core
|
|
|
13
13
|
```ts
|
|
14
14
|
import { createConsentify, defaultCategories } from '@consentify/core';
|
|
15
15
|
|
|
16
|
-
const
|
|
16
|
+
const consent = createConsentify({
|
|
17
17
|
policy: {
|
|
18
18
|
// Prefer to set a stable identifier derived from your policy document/version.
|
|
19
19
|
// If omitted, a deterministic hash of categories is used.
|
|
@@ -31,11 +31,11 @@ const manager = createConsentify({
|
|
|
31
31
|
});
|
|
32
32
|
|
|
33
33
|
// Ask user... then set decisions
|
|
34
|
-
|
|
34
|
+
consent.client.set({ analytics: true });
|
|
35
35
|
|
|
36
36
|
// Query on client
|
|
37
|
-
const canAnalytics =
|
|
38
|
-
const state =
|
|
37
|
+
const canAnalytics = consent.client.get('analytics'); // boolean
|
|
38
|
+
const state = consent.client.get(); // { decision: 'decided' | 'unset', snapshot? }
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
### SSR usage
|
|
@@ -44,15 +44,57 @@ Use the server API with a raw Cookie header. It never touches the DOM.
|
|
|
44
44
|
|
|
45
45
|
```ts
|
|
46
46
|
// Read consent on the server
|
|
47
|
-
const state =
|
|
47
|
+
const state = consent.server.get(request.headers.get('cookie'));
|
|
48
48
|
const canAnalytics = state.decision === 'decided' && !!state.snapshot.choices.analytics;
|
|
49
49
|
|
|
50
50
|
// Write consent on the server (e.g., after a POST to your consent endpoint)
|
|
51
|
-
const setCookieHeader =
|
|
51
|
+
const setCookieHeader = consent.server.set({ analytics: true }, request.headers.get('cookie'));
|
|
52
52
|
// Then attach header: res.setHeader('Set-Cookie', setCookieHeader)
|
|
53
53
|
|
|
54
54
|
// Clear consent cookie on the server
|
|
55
|
-
const clearCookieHeader =
|
|
55
|
+
const clearCookieHeader = consent.server.clear();
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### React integration
|
|
59
|
+
|
|
60
|
+
The client API is designed to work seamlessly with React 18+ `useSyncExternalStore`:
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
import { useSyncExternalStore } from 'react';
|
|
64
|
+
import { createConsentify, defaultCategories, type ConsentState, type DefaultCategory } from '@consentify/core';
|
|
65
|
+
|
|
66
|
+
// Create once at module level
|
|
67
|
+
const consent = createConsentify({
|
|
68
|
+
policy: { categories: defaultCategories, identifier: 'policy-v1' },
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Custom hook for React
|
|
72
|
+
function useConsent(): ConsentState<DefaultCategory> {
|
|
73
|
+
return useSyncExternalStore(
|
|
74
|
+
consent.client.subscribe,
|
|
75
|
+
consent.client.get,
|
|
76
|
+
consent.client.getServerSnapshot
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Usage in components
|
|
81
|
+
function ConsentBanner() {
|
|
82
|
+
const state = useConsent();
|
|
83
|
+
|
|
84
|
+
if (state.decision === 'decided') return null;
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<div>
|
|
88
|
+
<p>We use cookies to improve your experience.</p>
|
|
89
|
+
<button onClick={() => consent.client.set({ analytics: true, marketing: true })}>
|
|
90
|
+
Accept All
|
|
91
|
+
</button>
|
|
92
|
+
<button onClick={() => consent.client.set({ analytics: false, marketing: false })}>
|
|
93
|
+
Reject Optional
|
|
94
|
+
</button>
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
56
98
|
```
|
|
57
99
|
|
|
58
100
|
### API
|
|
@@ -63,10 +105,12 @@ Creates a consent manager bound to a `policy`. Cookie is the canonical store; yo
|
|
|
63
105
|
|
|
64
106
|
- `policy`: `{ categories: string[]; identifier: string }`
|
|
65
107
|
- `client`:
|
|
66
|
-
- `get(): ConsentState<T>` —
|
|
108
|
+
- `get(): ConsentState<T>` — returns cached consent state `{ decision: 'decided', snapshot } | { decision: 'unset' }`.
|
|
67
109
|
- `get(category: 'necessary' | T): boolean` — boolean check; `'necessary'` always returns `true`.
|
|
68
|
-
- `set(choices: Partial<Choices<T>>): void` — merges and saves; writes only if changed.
|
|
69
|
-
- `clear(): void` — removes stored consent (cookie and any mirror).
|
|
110
|
+
- `set(choices: Partial<Choices<T>>): void` — merges and saves; writes only if changed; notifies subscribers.
|
|
111
|
+
- `clear(): void` — removes stored consent (cookie and any mirror); notifies subscribers.
|
|
112
|
+
- `subscribe(callback: () => void): () => void` — subscribe to state changes (for React `useSyncExternalStore`).
|
|
113
|
+
- `getServerSnapshot(): ConsentState<T>` — returns `{ decision: 'unset' }` for SSR hydration.
|
|
70
114
|
- `server`:
|
|
71
115
|
- `get(cookieHeader: string | null | undefined): ConsentState<T>` — raw Cookie header in, state out.
|
|
72
116
|
- `set(choices: Partial<Choices<T>>, currentCookieHeader?: string): string` — returns `Set-Cookie` header string.
|
|
@@ -89,6 +133,7 @@ Notes:
|
|
|
89
133
|
|
|
90
134
|
- `'necessary'` is always `true` and cannot be disabled.
|
|
91
135
|
- The snapshot is invalidated automatically when the policy identity changes (identifier/hash).
|
|
136
|
+
- Client state is cached and subscribers are notified on changes for optimal React performance.
|
|
92
137
|
|
|
93
138
|
### Types
|
|
94
139
|
|
|
@@ -97,21 +142,52 @@ Notes:
|
|
|
97
142
|
- `Choices<T>` — map of consent by category plus `'necessary'`.
|
|
98
143
|
- `ConsentState<T>` — `{ decision: 'decided', snapshot } | { decision: 'unset' }`.
|
|
99
144
|
- `defaultCategories`/`DefaultCategory` — reusable defaults.
|
|
145
|
+
- `StorageKind` — `'cookie' | 'localStorage'`.
|
|
100
146
|
|
|
101
147
|
### Example: custom categories
|
|
102
148
|
|
|
103
149
|
```ts
|
|
104
150
|
type Cat = 'analytics' | 'ads' | 'functional';
|
|
105
|
-
const
|
|
106
|
-
policy: { categories: ['analytics','ads','functional'] as const, identifier: 'policy-v1' },
|
|
151
|
+
const consent = createConsentify({
|
|
152
|
+
policy: { categories: ['analytics', 'ads', 'functional'] as const, identifier: 'policy-v1' },
|
|
107
153
|
});
|
|
108
154
|
|
|
109
|
-
|
|
110
|
-
if (
|
|
155
|
+
consent.client.set({ analytics: true, ads: false });
|
|
156
|
+
if (consent.client.get('analytics')) {
|
|
111
157
|
// load analytics
|
|
112
158
|
}
|
|
113
159
|
```
|
|
114
160
|
|
|
161
|
+
### Example: Next.js App Router
|
|
162
|
+
|
|
163
|
+
```tsx
|
|
164
|
+
// lib/consent.ts
|
|
165
|
+
import { createConsentify, defaultCategories } from '@consentify/core';
|
|
166
|
+
|
|
167
|
+
export const consent = createConsentify({
|
|
168
|
+
policy: { categories: defaultCategories, identifier: 'policy-v1' },
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// app/layout.tsx (Server Component)
|
|
172
|
+
import { cookies } from 'next/headers';
|
|
173
|
+
import { consent } from '@/lib/consent';
|
|
174
|
+
|
|
175
|
+
export default async function RootLayout({ children }) {
|
|
176
|
+
const cookieStore = await cookies();
|
|
177
|
+
const state = consent.server.get(cookieStore.toString());
|
|
178
|
+
const canLoadAnalytics = state.decision === 'decided' && state.snapshot.choices.analytics;
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
<html>
|
|
182
|
+
<body>
|
|
183
|
+
{children}
|
|
184
|
+
{canLoadAnalytics && <AnalyticsScript />}
|
|
185
|
+
</body>
|
|
186
|
+
</html>
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
115
191
|
### Support
|
|
116
192
|
|
|
117
193
|
If you find this library useful, consider supporting its development:
|
|
@@ -122,5 +198,3 @@ If you find this library useful, consider supporting its development:
|
|
|
122
198
|
### License
|
|
123
199
|
|
|
124
200
|
MIT © 2025 [Roman Denysov](https://github.com/RomanDenysov)
|
|
125
|
-
|
|
126
|
-
|
package/dist/index.d.ts
CHANGED
|
@@ -83,6 +83,8 @@ export declare function createConsentify<Cs extends readonly string[]>(init: Cre
|
|
|
83
83
|
};
|
|
84
84
|
set: (choices: Partial<Choices<ArrToUnion<Cs>>>) => void;
|
|
85
85
|
clear: () => void;
|
|
86
|
+
subscribe: (callback: () => void) => (() => void);
|
|
87
|
+
getServerSnapshot: () => ConsentState<ArrToUnion<Cs>>;
|
|
86
88
|
};
|
|
87
89
|
};
|
|
88
90
|
export declare const defaultCategories: readonly ["preferences", "analytics", "marketing", "functional", "unclassified"];
|
package/dist/index.js
CHANGED
|
@@ -131,7 +131,6 @@ export function createConsentify(init) {
|
|
|
131
131
|
const writeClientRaw = (value) => {
|
|
132
132
|
const primary = firstAvailableStore();
|
|
133
133
|
writeToStore(primary, value);
|
|
134
|
-
// Mirror to cookie if requested anywhere in order and not already writing cookie
|
|
135
134
|
if (primary !== 'cookie' && storageOrder.includes('cookie'))
|
|
136
135
|
writeToStore('cookie', value);
|
|
137
136
|
};
|
|
@@ -190,14 +189,35 @@ export function createConsentify(init) {
|
|
|
190
189
|
return h;
|
|
191
190
|
}
|
|
192
191
|
};
|
|
193
|
-
|
|
192
|
+
// ========== NEW: Subscribe pattern for React ==========
|
|
193
|
+
const listeners = new Set();
|
|
194
|
+
const unsetState = { decision: 'unset' };
|
|
195
|
+
let cachedState = unsetState;
|
|
196
|
+
const syncState = () => {
|
|
194
197
|
const s = readClient();
|
|
195
|
-
|
|
198
|
+
if (!s) {
|
|
199
|
+
cachedState = unsetState;
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
cachedState = { decision: 'decided', snapshot: s };
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
const notifyListeners = () => {
|
|
206
|
+
listeners.forEach(cb => cb());
|
|
207
|
+
};
|
|
208
|
+
// Init cache on browser
|
|
209
|
+
if (isBrowser()) {
|
|
210
|
+
syncState();
|
|
211
|
+
}
|
|
212
|
+
function clientGet(category) {
|
|
213
|
+
// Return cached state for React compatibility
|
|
196
214
|
if (typeof category === 'undefined')
|
|
197
|
-
return
|
|
215
|
+
return cachedState;
|
|
198
216
|
if (category === 'necessary')
|
|
199
217
|
return true;
|
|
200
|
-
return
|
|
218
|
+
return cachedState.decision === 'decided'
|
|
219
|
+
? !!cachedState.snapshot.choices[category]
|
|
220
|
+
: false;
|
|
201
221
|
}
|
|
202
222
|
const client = {
|
|
203
223
|
get: clientGet,
|
|
@@ -209,14 +229,26 @@ export function createConsentify(init) {
|
|
|
209
229
|
givenAt: toISO(),
|
|
210
230
|
choices: normalize({ ...base, ...choices }),
|
|
211
231
|
};
|
|
212
|
-
writeClientIfChanged(next);
|
|
232
|
+
const changed = writeClientIfChanged(next);
|
|
233
|
+
if (changed) {
|
|
234
|
+
syncState();
|
|
235
|
+
notifyListeners();
|
|
236
|
+
}
|
|
213
237
|
},
|
|
214
238
|
clear: () => {
|
|
215
239
|
for (const k of new Set([...storageOrder, 'cookie']))
|
|
216
240
|
clearStore(k);
|
|
241
|
+
syncState();
|
|
242
|
+
notifyListeners();
|
|
243
|
+
},
|
|
244
|
+
// NEW: Subscribe for React useSyncExternalStore
|
|
245
|
+
subscribe: (callback) => {
|
|
246
|
+
listeners.add(callback);
|
|
247
|
+
return () => listeners.delete(callback);
|
|
217
248
|
},
|
|
249
|
+
// NEW: Server snapshot for SSR (always unset)
|
|
250
|
+
getServerSnapshot: () => unsetState,
|
|
218
251
|
};
|
|
219
|
-
// ---- single entry (DX aliases)
|
|
220
252
|
return {
|
|
221
253
|
policy: {
|
|
222
254
|
categories: init.policy.categories,
|