@consentify/core 0.1.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/LICENSE +23 -0
- package/README.md +126 -0
- package/dist/index.d.ts +90 -0
- package/dist/index.js +230 -0
- package/package.json +68 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Roman Denysov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
## @consentify/core
|
|
2
|
+
|
|
3
|
+
Headless cookie consent SDK — zero-deps, TypeScript-first, SSR-safe. Stores a compact snapshot in a cookie and provides a minimal, strongly-typed API.
|
|
4
|
+
|
|
5
|
+
### Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @consentify/core
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
### Quick start
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { createConsentify, defaultCategories } from '@consentify/core';
|
|
15
|
+
|
|
16
|
+
const manager = createConsentify({
|
|
17
|
+
policy: {
|
|
18
|
+
// Prefer to set a stable identifier derived from your policy document/version.
|
|
19
|
+
// If omitted, a deterministic hash of categories is used.
|
|
20
|
+
identifier: 'policy-v1',
|
|
21
|
+
categories: defaultCategories,
|
|
22
|
+
},
|
|
23
|
+
cookie: {
|
|
24
|
+
name: 'consentify',
|
|
25
|
+
path: '/',
|
|
26
|
+
sameSite: 'Lax',
|
|
27
|
+
secure: true,
|
|
28
|
+
},
|
|
29
|
+
// Cookie is canonical. Optionally mirror to localStorage for fast client reads.
|
|
30
|
+
// storage: ['localStorage', 'cookie']
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Ask user... then set decisions
|
|
34
|
+
manager.client.set({ analytics: true });
|
|
35
|
+
|
|
36
|
+
// Query on client
|
|
37
|
+
const canAnalytics = manager.client.get('analytics'); // boolean
|
|
38
|
+
const state = manager.client.get(); // { decision: 'decided' | 'unset', snapshot? }
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### SSR usage
|
|
42
|
+
|
|
43
|
+
Use the server API with a raw Cookie header. It never touches the DOM.
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
// Read consent on the server
|
|
47
|
+
const state = manager.server.get(request.headers.get('cookie'));
|
|
48
|
+
const canAnalytics = state.decision === 'decided' && !!state.snapshot.choices.analytics;
|
|
49
|
+
|
|
50
|
+
// Write consent on the server (e.g., after a POST to your consent endpoint)
|
|
51
|
+
const setCookieHeader = manager.server.set({ analytics: true }, request.headers.get('cookie'));
|
|
52
|
+
// Then attach header: res.setHeader('Set-Cookie', setCookieHeader)
|
|
53
|
+
|
|
54
|
+
// Clear consent cookie on the server
|
|
55
|
+
const clearCookieHeader = manager.server.clear();
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### API
|
|
59
|
+
|
|
60
|
+
#### createConsentify(init)
|
|
61
|
+
|
|
62
|
+
Creates a consent manager bound to a `policy`. Cookie is the canonical store; you can optionally mirror to `localStorage` for client-side speed. Returns:
|
|
63
|
+
|
|
64
|
+
- `policy`: `{ categories: string[]; identifier: string }`
|
|
65
|
+
- `client`:
|
|
66
|
+
- `get(): ConsentState<T>` — re-reads storage and returns `{ decision: 'decided', snapshot } | { decision: 'unset' }`.
|
|
67
|
+
- `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).
|
|
70
|
+
- `server`:
|
|
71
|
+
- `get(cookieHeader: string | null | undefined): ConsentState<T>` — raw Cookie header in, state out.
|
|
72
|
+
- `set(choices: Partial<Choices<T>>, currentCookieHeader?: string): string` — returns `Set-Cookie` header string.
|
|
73
|
+
- `clear(): string` — returns `Set-Cookie` header string to delete the cookie.
|
|
74
|
+
|
|
75
|
+
Options (init):
|
|
76
|
+
|
|
77
|
+
- `policy`: `{ categories: readonly T[]; identifier?: string }`
|
|
78
|
+
- `identifier` is recommended and should come from your actual policy version/content. If omitted, a deterministic hash of the categories is used.
|
|
79
|
+
- `cookie`:
|
|
80
|
+
- `name?: string` (default: `consentify`)
|
|
81
|
+
- `maxAgeSec?: number` (default: 1 year)
|
|
82
|
+
- `sameSite?: 'Lax' | 'Strict' | 'None'` (default: `'Lax'`)
|
|
83
|
+
- `secure?: boolean` (forced `true` when `sameSite==='None'`)
|
|
84
|
+
- `path?: string` (default: `/`)
|
|
85
|
+
- `domain?: string`
|
|
86
|
+
- `storage?: ('cookie' | 'localStorage')[]` (default: `['cookie']`)
|
|
87
|
+
|
|
88
|
+
Notes:
|
|
89
|
+
|
|
90
|
+
- `'necessary'` is always `true` and cannot be disabled.
|
|
91
|
+
- The snapshot is invalidated automatically when the policy identity changes (identifier/hash).
|
|
92
|
+
|
|
93
|
+
### Types
|
|
94
|
+
|
|
95
|
+
- `Policy<T>` — `{ identifier?: string; categories: readonly T[] }`.
|
|
96
|
+
- `Snapshot<T>` — `{ policy: string; givenAt: string; choices: Record<'necessary'|T, boolean> }`.
|
|
97
|
+
- `Choices<T>` — map of consent by category plus `'necessary'`.
|
|
98
|
+
- `ConsentState<T>` — `{ decision: 'decided', snapshot } | { decision: 'unset' }`.
|
|
99
|
+
- `defaultCategories`/`DefaultCategory` — reusable defaults.
|
|
100
|
+
|
|
101
|
+
### Example: custom categories
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
type Cat = 'analytics' | 'ads' | 'functional';
|
|
105
|
+
const manager = createConsentify({
|
|
106
|
+
policy: { categories: ['analytics','ads','functional'] as const, identifier: 'policy-v1' },
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
manager.client.set({ analytics: true, ads: false });
|
|
110
|
+
if (manager.client.get('analytics')) {
|
|
111
|
+
// load analytics
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Support
|
|
116
|
+
|
|
117
|
+
If you find this library useful, consider supporting its development:
|
|
118
|
+
|
|
119
|
+
- ⭐ [GitHub Sponsors](https://github.com/sponsors/RomanDenysov)
|
|
120
|
+
- ☕ [Ko-fi](https://ko-fi.com/romandenysov)
|
|
121
|
+
|
|
122
|
+
### License
|
|
123
|
+
|
|
124
|
+
MIT © 2025 [Roman Denysov](https://github.com/RomanDenysov)
|
|
125
|
+
|
|
126
|
+
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Literal type for the non-optional category that is always enabled.
|
|
3
|
+
*/
|
|
4
|
+
export type Necessary = 'necessary';
|
|
5
|
+
/**
|
|
6
|
+
* User-defined category identifier (e.g., 'analytics', 'marketing').
|
|
7
|
+
*/
|
|
8
|
+
export type UserCategory = string;
|
|
9
|
+
/**
|
|
10
|
+
* Map of consent choices for all categories, including the 'necessary' category.
|
|
11
|
+
* A value of `true` means the user granted consent for the category.
|
|
12
|
+
*/
|
|
13
|
+
export type Choices<T extends UserCategory> = Record<Necessary | T, boolean>;
|
|
14
|
+
/**
|
|
15
|
+
* Describes a cookie policy and its consent categories.
|
|
16
|
+
* @template T Category string union used by this policy.
|
|
17
|
+
*/
|
|
18
|
+
export interface Policy<T extends UserCategory> {
|
|
19
|
+
/**
|
|
20
|
+
* Optional stable identifier for your policy. Prefer supplying a value derived from
|
|
21
|
+
* your actual policy content/version (e.g., a hash of the policy document).
|
|
22
|
+
* If omitted, a deterministic hash of the provided categories (and this identifier when present)
|
|
23
|
+
* will be used to key snapshots.
|
|
24
|
+
*/
|
|
25
|
+
identifier?: string;
|
|
26
|
+
categories: readonly T[];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Immutable snapshot of a user's consent decision for a specific policy version.
|
|
30
|
+
* @template T Category string union captured in the snapshot.
|
|
31
|
+
*/
|
|
32
|
+
export interface Snapshot<T extends UserCategory> {
|
|
33
|
+
policy: string;
|
|
34
|
+
givenAt: string;
|
|
35
|
+
choices: Choices<T>;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* High-level consent state derived from the presence of a valid snapshot.
|
|
39
|
+
* When no valid snapshot exists for the current policy version, the state is `unset`.
|
|
40
|
+
*/
|
|
41
|
+
export type ConsentState<T extends UserCategory> = {
|
|
42
|
+
decision: 'unset';
|
|
43
|
+
} | {
|
|
44
|
+
decision: 'decided';
|
|
45
|
+
snapshot: Snapshot<T>;
|
|
46
|
+
};
|
|
47
|
+
type ArrToUnion<T extends readonly string[]> = T[number];
|
|
48
|
+
export type StorageKind = 'cookie' | 'localStorage';
|
|
49
|
+
export interface CreateConsentifyInit<Cs extends readonly string[]> {
|
|
50
|
+
policy: {
|
|
51
|
+
categories: Cs;
|
|
52
|
+
identifier?: string;
|
|
53
|
+
};
|
|
54
|
+
cookie?: {
|
|
55
|
+
name?: string;
|
|
56
|
+
maxAgeSec?: number;
|
|
57
|
+
sameSite?: 'Lax' | 'Strict' | 'None';
|
|
58
|
+
secure?: boolean;
|
|
59
|
+
path?: string;
|
|
60
|
+
domain?: string;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Client-side storage priority. Server-side access is cookie-only.
|
|
64
|
+
* Supported: 'cookie' (canonical), 'localStorage' (optional mirror for fast reads)
|
|
65
|
+
* Default: ['cookie']
|
|
66
|
+
*/
|
|
67
|
+
storage?: StorageKind[];
|
|
68
|
+
}
|
|
69
|
+
export declare function createConsentify<Cs extends readonly string[]>(init: CreateConsentifyInit<Cs>): {
|
|
70
|
+
readonly policy: {
|
|
71
|
+
readonly categories: Cs;
|
|
72
|
+
readonly identifier: string;
|
|
73
|
+
};
|
|
74
|
+
readonly server: {
|
|
75
|
+
get: (cookieHeader: string | null | undefined) => ConsentState<ArrToUnion<Cs>>;
|
|
76
|
+
set: (choices: Partial<Choices<ArrToUnion<Cs>>>, currentCookieHeader?: string) => string;
|
|
77
|
+
clear: () => string;
|
|
78
|
+
};
|
|
79
|
+
readonly client: {
|
|
80
|
+
get: {
|
|
81
|
+
(): ConsentState<ArrToUnion<Cs>>;
|
|
82
|
+
(category: "necessary" | ArrToUnion<Cs>): boolean;
|
|
83
|
+
};
|
|
84
|
+
set: (choices: Partial<Choices<ArrToUnion<Cs>>>) => void;
|
|
85
|
+
clear: () => void;
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
export declare const defaultCategories: readonly ["preferences", "analytics", "marketing", "functional", "unclassified"];
|
|
89
|
+
export type DefaultCategory = typeof defaultCategories[number];
|
|
90
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
function stableStringify(o) {
|
|
2
|
+
if (o === null || typeof o !== 'object')
|
|
3
|
+
return JSON.stringify(o);
|
|
4
|
+
if (Array.isArray(o))
|
|
5
|
+
return `[${o.map(stableStringify).join(',')}]`;
|
|
6
|
+
const e = Object.entries(o).sort((a, b) => a[0].localeCompare(b[0]));
|
|
7
|
+
return `{${e.map(([k, v]) => JSON.stringify(k) + ':' + stableStringify(v)).join(',')}}`;
|
|
8
|
+
}
|
|
9
|
+
function fnv1a(str) {
|
|
10
|
+
let h = 0x811c9dc5 >>> 0;
|
|
11
|
+
for (let i = 0; i < str.length; i++) {
|
|
12
|
+
h ^= str.charCodeAt(i);
|
|
13
|
+
h = (h + ((h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24))) >>> 0;
|
|
14
|
+
}
|
|
15
|
+
return ('00000000' + h.toString(16)).slice(-8);
|
|
16
|
+
}
|
|
17
|
+
function hashPolicy(categories, identifier) {
|
|
18
|
+
// Deterministic identity for the policy. If you provide `identifier`, it is folded into the hash,
|
|
19
|
+
// but consider using `identifier` itself as the canonical version key for clarity.
|
|
20
|
+
return fnv1a(stableStringify({ categories: [...categories].sort(), identifier: identifier ?? null }));
|
|
21
|
+
}
|
|
22
|
+
// --- Internals ---
|
|
23
|
+
const DEFAULT_COOKIE = 'consentify';
|
|
24
|
+
const enc = (o) => encodeURIComponent(JSON.stringify(o));
|
|
25
|
+
const dec = (s) => { try {
|
|
26
|
+
return JSON.parse(decodeURIComponent(s));
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return null;
|
|
30
|
+
} };
|
|
31
|
+
const toISO = () => new Date().toISOString();
|
|
32
|
+
function readCookie(name, cookieStr) {
|
|
33
|
+
const src = cookieStr ?? (typeof document !== 'undefined' ? document.cookie : '');
|
|
34
|
+
if (!src)
|
|
35
|
+
return null;
|
|
36
|
+
const m = src.split(';').map(v => v.trim()).find(v => v.startsWith(name + '='));
|
|
37
|
+
return m ? m.slice(name.length + 1) : null;
|
|
38
|
+
}
|
|
39
|
+
function writeCookie(name, value, opt) {
|
|
40
|
+
if (typeof document === 'undefined')
|
|
41
|
+
return;
|
|
42
|
+
let c = `${name}=${value}; Path=${opt.path}; Max-Age=${opt.maxAgeSec}; SameSite=${opt.sameSite}`;
|
|
43
|
+
if (opt.domain)
|
|
44
|
+
c += `; Domain=${opt.domain}`;
|
|
45
|
+
if (opt.secure)
|
|
46
|
+
c += `; Secure`;
|
|
47
|
+
document.cookie = c;
|
|
48
|
+
}
|
|
49
|
+
// --- Unified Factory (single entry point) ---
|
|
50
|
+
export function createConsentify(init) {
|
|
51
|
+
const policyHash = init.policy.identifier ?? hashPolicy(init.policy.categories);
|
|
52
|
+
const cookieName = init.cookie?.name ?? DEFAULT_COOKIE;
|
|
53
|
+
const sameSite = init.cookie?.sameSite ?? 'Lax';
|
|
54
|
+
const cookieCfg = {
|
|
55
|
+
path: init.cookie?.path ?? '/',
|
|
56
|
+
maxAgeSec: init.cookie?.maxAgeSec ?? 60 * 60 * 24 * 365,
|
|
57
|
+
sameSite,
|
|
58
|
+
secure: sameSite === 'None' ? true : (init.cookie?.secure ?? true),
|
|
59
|
+
domain: init.cookie?.domain,
|
|
60
|
+
};
|
|
61
|
+
const storageOrder = (init.storage && init.storage.length > 0) ? init.storage : ['cookie'];
|
|
62
|
+
const allowed = new Set(['necessary', ...init.policy.categories]);
|
|
63
|
+
const normalize = (choices) => {
|
|
64
|
+
const base = { necessary: true };
|
|
65
|
+
for (const c of init.policy.categories)
|
|
66
|
+
base[c] = false;
|
|
67
|
+
if (choices) {
|
|
68
|
+
for (const [k, v] of Object.entries(choices)) {
|
|
69
|
+
if (allowed.has(k))
|
|
70
|
+
base[k] = !!v;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
base.necessary = true;
|
|
74
|
+
return base;
|
|
75
|
+
};
|
|
76
|
+
// --- client-side storage helpers ---
|
|
77
|
+
const isBrowser = () => typeof window !== 'undefined' && typeof document !== 'undefined';
|
|
78
|
+
const canLocal = () => { try {
|
|
79
|
+
return isBrowser() && !!window.localStorage;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return false;
|
|
83
|
+
} };
|
|
84
|
+
const readFromStore = (kind) => {
|
|
85
|
+
switch (kind) {
|
|
86
|
+
case 'cookie': return readCookie(cookieName);
|
|
87
|
+
case 'localStorage': return canLocal() ? window.localStorage.getItem(cookieName) : null;
|
|
88
|
+
default: return null;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
const writeToStore = (kind, value) => {
|
|
92
|
+
switch (kind) {
|
|
93
|
+
case 'cookie':
|
|
94
|
+
writeCookie(cookieName, value, cookieCfg);
|
|
95
|
+
break;
|
|
96
|
+
case 'localStorage':
|
|
97
|
+
if (canLocal())
|
|
98
|
+
window.localStorage.setItem(cookieName, value);
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
const clearStore = (kind) => {
|
|
103
|
+
switch (kind) {
|
|
104
|
+
case 'cookie':
|
|
105
|
+
if (isBrowser())
|
|
106
|
+
document.cookie = `${cookieName}=; Path=${cookieCfg.path}; Max-Age=0; SameSite=${cookieCfg.sameSite}${cookieCfg.domain ? `; Domain=${cookieCfg.domain}` : ''}${cookieCfg.secure ? '; Secure' : ''}`;
|
|
107
|
+
break;
|
|
108
|
+
case 'localStorage':
|
|
109
|
+
if (canLocal())
|
|
110
|
+
window.localStorage.removeItem(cookieName);
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
const firstAvailableStore = () => {
|
|
115
|
+
for (const k of storageOrder) {
|
|
116
|
+
if (k === 'cookie')
|
|
117
|
+
return 'cookie';
|
|
118
|
+
if (k === 'localStorage' && canLocal())
|
|
119
|
+
return 'localStorage';
|
|
120
|
+
}
|
|
121
|
+
return 'cookie';
|
|
122
|
+
};
|
|
123
|
+
const readClientRaw = () => {
|
|
124
|
+
for (const k of storageOrder) {
|
|
125
|
+
const v = readFromStore(k);
|
|
126
|
+
if (v)
|
|
127
|
+
return v;
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
};
|
|
131
|
+
const writeClientRaw = (value) => {
|
|
132
|
+
const primary = firstAvailableStore();
|
|
133
|
+
writeToStore(primary, value);
|
|
134
|
+
// Mirror to cookie if requested anywhere in order and not already writing cookie
|
|
135
|
+
if (primary !== 'cookie' && storageOrder.includes('cookie'))
|
|
136
|
+
writeToStore('cookie', value);
|
|
137
|
+
};
|
|
138
|
+
// --- read helpers ---
|
|
139
|
+
const readClient = () => {
|
|
140
|
+
const raw = readClientRaw();
|
|
141
|
+
const s = raw ? dec(raw) : null;
|
|
142
|
+
if (!s)
|
|
143
|
+
return null;
|
|
144
|
+
if (s.policy !== policyHash)
|
|
145
|
+
return null;
|
|
146
|
+
return s;
|
|
147
|
+
};
|
|
148
|
+
const writeClientIfChanged = (next) => {
|
|
149
|
+
const prev = readClient();
|
|
150
|
+
const same = !!(prev && prev.policy === next.policy && JSON.stringify(prev.choices) === JSON.stringify(next.choices));
|
|
151
|
+
if (!same)
|
|
152
|
+
writeClientRaw(enc(next));
|
|
153
|
+
return !same;
|
|
154
|
+
};
|
|
155
|
+
function buildSetCookieHeader(name, value, opt) {
|
|
156
|
+
let header = `${name}=${value}; Path=${opt.path}; Max-Age=${opt.maxAgeSec}; SameSite=${opt.sameSite}`;
|
|
157
|
+
if (opt.domain)
|
|
158
|
+
header += `; Domain=${opt.domain}`;
|
|
159
|
+
if (opt.secure)
|
|
160
|
+
header += `; Secure`;
|
|
161
|
+
return header;
|
|
162
|
+
}
|
|
163
|
+
// ---- server API
|
|
164
|
+
const server = {
|
|
165
|
+
get: (cookieHeader) => {
|
|
166
|
+
const raw = cookieHeader ? readCookie(cookieName, cookieHeader) : null;
|
|
167
|
+
const s = raw ? dec(raw) : null;
|
|
168
|
+
if (!s)
|
|
169
|
+
return { decision: 'unset' };
|
|
170
|
+
if (s.policy !== policyHash)
|
|
171
|
+
return { decision: 'unset' };
|
|
172
|
+
return { decision: 'decided', snapshot: s };
|
|
173
|
+
},
|
|
174
|
+
set: (choices, currentCookieHeader) => {
|
|
175
|
+
const prev = currentCookieHeader ? server.get(currentCookieHeader) : { decision: 'unset' };
|
|
176
|
+
const base = prev.decision === 'decided' ? prev.snapshot.choices : normalize();
|
|
177
|
+
const snapshot = {
|
|
178
|
+
policy: policyHash,
|
|
179
|
+
givenAt: toISO(),
|
|
180
|
+
choices: normalize({ ...base, ...choices }),
|
|
181
|
+
};
|
|
182
|
+
return buildSetCookieHeader(cookieName, enc(snapshot), cookieCfg);
|
|
183
|
+
},
|
|
184
|
+
clear: () => {
|
|
185
|
+
let h = `${cookieName}=; Path=${cookieCfg.path}; Max-Age=0; SameSite=${cookieCfg.sameSite}`;
|
|
186
|
+
if (cookieCfg.domain)
|
|
187
|
+
h += `; Domain=${cookieCfg.domain}`;
|
|
188
|
+
if (cookieCfg.secure)
|
|
189
|
+
h += `; Secure`;
|
|
190
|
+
return h;
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
function clientGet(category) {
|
|
194
|
+
const s = readClient();
|
|
195
|
+
const state = s ? { decision: 'decided', snapshot: s } : { decision: 'unset' };
|
|
196
|
+
if (typeof category === 'undefined')
|
|
197
|
+
return state;
|
|
198
|
+
if (category === 'necessary')
|
|
199
|
+
return true;
|
|
200
|
+
return state.decision === 'decided' ? !!state.snapshot.choices[category] : false;
|
|
201
|
+
}
|
|
202
|
+
const client = {
|
|
203
|
+
get: clientGet,
|
|
204
|
+
set: (choices) => {
|
|
205
|
+
const prev = client.get();
|
|
206
|
+
const base = prev.decision === 'decided' ? prev.snapshot.choices : normalize();
|
|
207
|
+
const next = {
|
|
208
|
+
policy: policyHash,
|
|
209
|
+
givenAt: toISO(),
|
|
210
|
+
choices: normalize({ ...base, ...choices }),
|
|
211
|
+
};
|
|
212
|
+
writeClientIfChanged(next);
|
|
213
|
+
},
|
|
214
|
+
clear: () => {
|
|
215
|
+
for (const k of new Set([...storageOrder, 'cookie']))
|
|
216
|
+
clearStore(k);
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
// ---- single entry (DX aliases)
|
|
220
|
+
return {
|
|
221
|
+
policy: {
|
|
222
|
+
categories: init.policy.categories,
|
|
223
|
+
identifier: policyHash,
|
|
224
|
+
},
|
|
225
|
+
server,
|
|
226
|
+
client,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
// Common predefined category names you can reuse in your policy.
|
|
230
|
+
export const defaultCategories = ['preferences', 'analytics', 'marketing', 'functional', 'unclassified'];
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@consentify/core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Minimal headless cookie consent SDK (TypeScript, SSR-ready, lazy-init).",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Roman Denysov",
|
|
7
|
+
"url": "https://github.com/RomanDenysov"
|
|
8
|
+
},
|
|
9
|
+
"funding": [
|
|
10
|
+
{
|
|
11
|
+
"type": "github",
|
|
12
|
+
"url": "https://github.com/sponsors/RomanDenysov"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"type": "ko-fi",
|
|
16
|
+
"url": "https://ko-fi.com/romandenysov"
|
|
17
|
+
}
|
|
18
|
+
],
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=20.0.0",
|
|
21
|
+
"pnpm": ">=9"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"cookie",
|
|
25
|
+
"consent",
|
|
26
|
+
"gdpr",
|
|
27
|
+
"ccpa",
|
|
28
|
+
"privacy",
|
|
29
|
+
"typescript",
|
|
30
|
+
"ssr"
|
|
31
|
+
],
|
|
32
|
+
"sideEffects": false,
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/RomanDenysov/consentify.git",
|
|
36
|
+
"directory": "packages/core"
|
|
37
|
+
},
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/RomanDenysov/consentify/issues"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://github.com/RomanDenysov/consentify#readme",
|
|
42
|
+
"exports": {
|
|
43
|
+
".": {
|
|
44
|
+
"import": "./dist/index.js",
|
|
45
|
+
"types": "./dist/index.d.ts"
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"module": "./dist/index.js",
|
|
49
|
+
"types": "./dist/index.d.ts",
|
|
50
|
+
"files": [
|
|
51
|
+
"dist/**/*",
|
|
52
|
+
"README.md",
|
|
53
|
+
"LICENSE"
|
|
54
|
+
],
|
|
55
|
+
"license": "MIT",
|
|
56
|
+
"type": "module",
|
|
57
|
+
"publishConfig": {
|
|
58
|
+
"access": "public"
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"rimraf": "6.0.1",
|
|
62
|
+
"typescript": "5.9.3",
|
|
63
|
+
"@types/node": "24.7.0"
|
|
64
|
+
},
|
|
65
|
+
"scripts": {
|
|
66
|
+
"build": "rimraf dist && tsc -p tsconfig.build.json"
|
|
67
|
+
}
|
|
68
|
+
}
|