@cookiemunch/sdk 0.0.0-stage → 0.1.1
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 +59 -2
- package/dist/chunk-G7ORTXTK.js +104 -0
- package/dist/chunk-G7ORTXTK.js.map +1 -0
- package/dist/consent.cjs +132 -0
- package/dist/consent.cjs.map +1 -0
- package/dist/consent.d.cts +88 -0
- package/dist/consent.d.ts +88 -0
- package/dist/consent.js +15 -0
- package/dist/consent.js.map +1 -0
- package/dist/index.cjs +475 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1714 -0
- package/dist/index.d.ts +1714 -0
- package/dist/index.js +353 -0
- package/dist/index.js.map +1 -0
- package/package.json +37 -3
package/README.md
CHANGED
|
@@ -1,3 +1,60 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @cookiemunch/sdk
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A small, typed REST client for the Cookie Munch Developer API (the `/v1` surface).
|
|
4
|
+
Authenticate with an API key — the organization is derived server-side from the key,
|
|
5
|
+
so you never pass an `orgId`.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { createCookieMunch, CookieMunchApiError } from '@cookiemunch/sdk';
|
|
9
|
+
|
|
10
|
+
const fc = createCookieMunch({
|
|
11
|
+
apiKey: process.env.COOKIEMUNCH_API_KEY!, // "fck_…"
|
|
12
|
+
baseUrl: 'https://cmp.example.com', // "/v1" is appended automatically
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const sites = await fc.sites.list();
|
|
16
|
+
const site = await fc.sites.create({ domain: 'example.com' }); // cbid auto-generated
|
|
17
|
+
const stats = await fc.consent.stats(site.cbid, { from: 0, to: Date.now() });
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
await fc.sites.get('does-not-exist');
|
|
21
|
+
} catch (e) {
|
|
22
|
+
if (e instanceof CookieMunchApiError) console.error(e.status, e.message);
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`fetch` is injectable via `createCookieMunch({ ..., fetch })` for tests or custom
|
|
27
|
+
runtimes; it defaults to the global `fetch`.
|
|
28
|
+
|
|
29
|
+
## Methods
|
|
30
|
+
|
|
31
|
+
- `me()`
|
|
32
|
+
- `sites.list() / create(input) / get(cbid) / delete(cbid) / getConfig(cbid) / putConfig(cbid, config)`
|
|
33
|
+
- `consent.stats(cbid, {from?,to?}) / log(cbid, {from?,to?,limit?}) / export(cbid, {from?,to?}) / receipt(cbid, stamp)`
|
|
34
|
+
- `dsar.list() / create(input) / advance(id, toStatus)`
|
|
35
|
+
- `vendors.list() / create(input)`
|
|
36
|
+
- `ropa.list() / create(input)`
|
|
37
|
+
- `webhooks.list() / create(input)`
|
|
38
|
+
|
|
39
|
+
`export()` returns the raw CSV string; all other methods return parsed JSON. Any
|
|
40
|
+
non-2xx response throws a `CookieMunchApiError { status, code?, message }`. Error `code`
|
|
41
|
+
values are catalogued in [`docs/api/error-codes.md`](../../docs/api/error-codes.md).
|
|
42
|
+
|
|
43
|
+
## Verifying webhooks
|
|
44
|
+
|
|
45
|
+
`verifyWebhookSignature(...)` validates an inbound webhook's `X-CookieMunch-Signature`
|
|
46
|
+
(HMAC-SHA256 over `` `${timestamp}.${rawBody}` ``) and its freshness. It's the authoritative
|
|
47
|
+
verifier — always matches the server. Fail-closed; returns a boolean.
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import { verifyWebhookSignature } from '@cookiemunch/sdk';
|
|
51
|
+
|
|
52
|
+
const ok = verifyWebhookSignature({
|
|
53
|
+
payload: rawBody, // exact request body bytes (a string)
|
|
54
|
+
signature: headers['x-cookiemunch-signature'],
|
|
55
|
+
timestamp: headers['x-cookiemunch-timestamp'],
|
|
56
|
+
secret: process.env.CM_WEBHOOK_SECRET!, // the whsec_… shown once on create
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Full recipe (Express, edge/WebCrypto, replay protection): [`docs/api/webhook-signing.md`](../../docs/api/webhook-signing.md).
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// src/consent.ts
|
|
2
|
+
var CONSENT_COOKIE_NAME = "CookieMunch";
|
|
3
|
+
function cookieHeaderOf(src) {
|
|
4
|
+
if (typeof src === "string") return src;
|
|
5
|
+
if (!src || typeof src !== "object") return "";
|
|
6
|
+
const h = src.headers;
|
|
7
|
+
if (!h) return "";
|
|
8
|
+
if (typeof h.get === "function") {
|
|
9
|
+
return h.get("cookie") ?? "";
|
|
10
|
+
}
|
|
11
|
+
const c = h.cookie;
|
|
12
|
+
return typeof c === "string" ? c : "";
|
|
13
|
+
}
|
|
14
|
+
function cookieValue(header, name) {
|
|
15
|
+
for (const part of header.split(";")) {
|
|
16
|
+
const eq = part.indexOf("=");
|
|
17
|
+
if (eq === -1) continue;
|
|
18
|
+
if (part.slice(0, eq).trim() === name) return part.slice(eq + 1).trim();
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
function bool(v) {
|
|
23
|
+
return typeof v === "boolean";
|
|
24
|
+
}
|
|
25
|
+
function decode(raw) {
|
|
26
|
+
try {
|
|
27
|
+
return decodeURIComponent(raw);
|
|
28
|
+
} catch {
|
|
29
|
+
return raw;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function normalize(o) {
|
|
33
|
+
if (!o || typeof o !== "object") return null;
|
|
34
|
+
const r = o;
|
|
35
|
+
if (!bool(r.preferences) || !bool(r.statistics) || !bool(r.marketing)) return null;
|
|
36
|
+
if (r.method !== "explicit" && r.method !== "implied") return null;
|
|
37
|
+
if (typeof r.ver !== "number" || typeof r.utc !== "number") return null;
|
|
38
|
+
if (typeof r.stamp !== "string" && typeof r.stamp !== "number") return null;
|
|
39
|
+
if (typeof r.region !== "string") return null;
|
|
40
|
+
const out = {
|
|
41
|
+
necessary: true,
|
|
42
|
+
// never trust a cookie that claims necessary:false
|
|
43
|
+
preferences: r.preferences,
|
|
44
|
+
statistics: r.statistics,
|
|
45
|
+
marketing: r.marketing,
|
|
46
|
+
method: r.method,
|
|
47
|
+
ver: r.ver,
|
|
48
|
+
utc: r.utc,
|
|
49
|
+
region: r.region,
|
|
50
|
+
stamp: String(r.stamp)
|
|
51
|
+
};
|
|
52
|
+
if (r.custom && typeof r.custom === "object" && !Array.isArray(r.custom)) {
|
|
53
|
+
const custom = {};
|
|
54
|
+
for (const [k, v] of Object.entries(r.custom)) if (bool(v)) custom[k] = v;
|
|
55
|
+
if (Object.keys(custom).length > 0) out.custom = custom;
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
function parseConsentValue(raw) {
|
|
60
|
+
if (!raw) return null;
|
|
61
|
+
const decoded = decode(raw);
|
|
62
|
+
try {
|
|
63
|
+
return normalize(JSON.parse(decoded));
|
|
64
|
+
} catch {
|
|
65
|
+
}
|
|
66
|
+
if (!decoded.includes("necessary")) return null;
|
|
67
|
+
const json = decoded.replace(/'/g, '"').replace(/([{,]\s*)([A-Za-z_][\w]*)\s*:/g, '$1"$2":');
|
|
68
|
+
try {
|
|
69
|
+
return normalize(JSON.parse(json));
|
|
70
|
+
} catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
75
|
+
function readConsent(source, opts = {}) {
|
|
76
|
+
const raw = cookieValue(cookieHeaderOf(source), opts.cookieName ?? CONSENT_COOKIE_NAME);
|
|
77
|
+
const consent = raw ? parseConsentValue(raw) : null;
|
|
78
|
+
if (!consent) return null;
|
|
79
|
+
if (typeof opts.minVersion === "number" && consent.ver < opts.minVersion) return null;
|
|
80
|
+
if (typeof opts.expiryDays === "number" && opts.expiryDays > 0) {
|
|
81
|
+
if ((opts.now ?? Date.now()) - consent.utc > opts.expiryDays * DAY_MS) return null;
|
|
82
|
+
}
|
|
83
|
+
return consent;
|
|
84
|
+
}
|
|
85
|
+
function getConsent(source, cookieName = CONSENT_COOKIE_NAME) {
|
|
86
|
+
return readConsent(source, { cookieName });
|
|
87
|
+
}
|
|
88
|
+
function isAllowed(consent, category) {
|
|
89
|
+
if (category === "necessary") return true;
|
|
90
|
+
if (!consent) return false;
|
|
91
|
+
if (category === "preferences") return consent.preferences === true;
|
|
92
|
+
if (category === "statistics") return consent.statistics === true;
|
|
93
|
+
if (category === "marketing") return consent.marketing === true;
|
|
94
|
+
return consent.custom?.[category] === true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export {
|
|
98
|
+
CONSENT_COOKIE_NAME,
|
|
99
|
+
parseConsentValue,
|
|
100
|
+
readConsent,
|
|
101
|
+
getConsent,
|
|
102
|
+
isAllowed
|
|
103
|
+
};
|
|
104
|
+
//# sourceMappingURL=chunk-G7ORTXTK.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/consent.ts"],"sourcesContent":["/**\n * Server-side consent reading. The Cookie Munch embed stores the visitor's decision in a\n * first-party `CookieMunch` cookie (URL-encoded JSON). This lets a server / edge worker read\n * that decision from the incoming request — to gate server-rendered tags or SSR'd third-party\n * scripts BEFORE they hit the browser — without shipping the whole embed. Zero-dependency and\n * runtime-agnostic: accepts a raw Cookie header, a Node `req` (`req.headers.cookie`), or a\n * Fetch `Request` (`request.headers.get('cookie')`).\n *\n * import { readConsent, isAllowed } from '@cookiemunch/sdk/consent';\n * const consent = readConsent(req.headers.cookie); // ServerConsent | null\n * if (isAllowed(consent, 'statistics')) { …render analytics tag… }\n *\n * The contract is documented at https://cookiemunch.net/docs/consent-cookie. It accepts\n * exactly what the embed accepts, so a server never grants what the browser would refuse:\n * strict JSON (URL-encoded or not) and the relaxed object literal Cookiebot writes.\n *\n * This module imports nothing — `@cookiemunch/sdk/consent` pulls in no API client.\n */\n\n/** The first-party cookie the embed writes (mirrors @cookiemunch/core COOKIE_NAME). */\nexport const CONSENT_COOKIE_NAME = 'CookieMunch';\n\n/** A visitor's consent decision, read from the request cookie. `necessary` is always true. */\nexport interface ServerConsent {\n necessary: true;\n preferences: boolean;\n statistics: boolean;\n marketing: boolean;\n /** How consent was given. `implied` = not yet an explicit choice. */\n method: 'explicit' | 'implied';\n /** Custom (non-standard) categories, when the site defines any. */\n custom?: Record<string, boolean>;\n /** Consent-policy version the decision was made under (see `minVersion`). */\n ver: number;\n /** When the decision was made, epoch milliseconds (see `expiryDays`). */\n utc: number;\n /** Region code the decision was made in (`unknown` when undetected). */\n region: string;\n /** The anonymous consent-receipt id tying this browser to the consent log. */\n stamp: string;\n}\n\n/** The site's consent policy, so the server applies the same rules the embed does. */\nexport interface ReadConsentOptions {\n /** Cookie name. Default `CookieMunch`. */\n cookieName?: string;\n /**\n * The site's current consent version (`consent.version`, or the config `ver` when that is\n * higher). A decision made under an older version is not honoured by the embed — the\n * visitor is asked again — so it reads as `null` here too.\n */\n minVersion?: number;\n /** The site's `consent.expiryDays`: a decision older than this reads as `null`. */\n expiryDays?: number;\n /** Clock for `expiryDays`, epoch ms. Default `Date.now()`. */\n now?: number;\n}\n\n/** Anything we can pull a Cookie header off of. */\nexport type ConsentSource =\n | string\n | null\n | undefined\n | { headers?: { cookie?: string | null; get?(name: string): string | null } | Record<string, unknown> };\n\nfunction cookieHeaderOf(src: ConsentSource): string {\n if (typeof src === 'string') return src;\n if (!src || typeof src !== 'object') return '';\n const h = src.headers;\n if (!h) return '';\n // Fetch Request: headers.get('cookie'); Node: headers.cookie.\n if (typeof (h as { get?(n: string): string | null }).get === 'function') {\n return (h as { get(n: string): string | null }).get('cookie') ?? '';\n }\n const c = (h as Record<string, unknown>).cookie;\n return typeof c === 'string' ? c : '';\n}\n\n/** Extract one cookie's raw value from a Cookie header (no decoding). */\nfunction cookieValue(header: string, name: string): string | null {\n for (const part of header.split(';')) {\n const eq = part.indexOf('=');\n if (eq === -1) continue;\n if (part.slice(0, eq).trim() === name) return part.slice(eq + 1).trim();\n }\n return null;\n}\n\nfunction bool(v: unknown): v is boolean {\n return typeof v === 'boolean';\n}\n\nfunction decode(raw: string): string {\n try {\n return decodeURIComponent(raw);\n } catch {\n return raw; // not URL-encoded; use as-is\n }\n}\n\n/** The embed's own validation (core cookie.ts `normalize`), field for field. */\nfunction normalize(o: unknown): ServerConsent | null {\n if (!o || typeof o !== 'object') return null;\n const r = o as Record<string, unknown>;\n if (!bool(r.preferences) || !bool(r.statistics) || !bool(r.marketing)) return null;\n if (r.method !== 'explicit' && r.method !== 'implied') return null;\n if (typeof r.ver !== 'number' || typeof r.utc !== 'number') return null;\n if (typeof r.stamp !== 'string' && typeof r.stamp !== 'number') return null;\n if (typeof r.region !== 'string') return null;\n const out: ServerConsent = {\n necessary: true, // never trust a cookie that claims necessary:false\n preferences: r.preferences,\n statistics: r.statistics,\n marketing: r.marketing,\n method: r.method,\n ver: r.ver,\n utc: r.utc,\n region: r.region,\n stamp: String(r.stamp),\n };\n if (r.custom && typeof r.custom === 'object' && !Array.isArray(r.custom)) {\n const custom: Record<string, boolean> = {};\n for (const [k, v] of Object.entries(r.custom as Record<string, unknown>)) if (bool(v)) custom[k] = v;\n if (Object.keys(custom).length > 0) out.custom = custom;\n }\n return out;\n}\n\n/**\n * Parse a raw cookie value into a validated ServerConsent, or null. Accepts the two formats\n * the embed reads: URL-encoded (or plain) strict JSON, and Cookiebot's relaxed object\n * literal — `{stamp:'…',necessary:true,…}` — left behind by a site migrating from Cookiebot.\n */\nexport function parseConsentValue(raw: string): ServerConsent | null {\n if (!raw) return null;\n const decoded = decode(raw);\n try {\n return normalize(JSON.parse(decoded));\n } catch {\n /* not strict JSON — try the Cookiebot literal */\n }\n if (!decoded.includes('necessary')) return null;\n const json = decoded.replace(/'/g, '\"').replace(/([{,]\\s*)([A-Za-z_][\\w]*)\\s*:/g, '$1\"$2\":');\n try {\n return normalize(JSON.parse(json));\n } catch {\n return null;\n }\n}\n\nconst DAY_MS = 24 * 60 * 60 * 1000;\n\n/**\n * Read the visitor's consent from a request: a raw `Cookie` header, a Node `req`, or a Fetch\n * `Request`. Returns null when there is no decision the embed would honour — no cookie, an\n * unparseable one, or (with the options) one made under an older version or past expiry.\n */\nexport function readConsent(source: ConsentSource, opts: ReadConsentOptions = {}): ServerConsent | null {\n const raw = cookieValue(cookieHeaderOf(source), opts.cookieName ?? CONSENT_COOKIE_NAME);\n const consent = raw ? parseConsentValue(raw) : null;\n if (!consent) return null;\n if (typeof opts.minVersion === 'number' && consent.ver < opts.minVersion) return null;\n if (typeof opts.expiryDays === 'number' && opts.expiryDays > 0) {\n if ((opts.now ?? Date.now()) - consent.utc > opts.expiryDays * DAY_MS) return null;\n }\n return consent;\n}\n\n/**\n * Read the visitor's consent from a request. The original spelling of {@link readConsent},\n * kept for existing callers; the second argument is the cookie name.\n */\nexport function getConsent(source: ConsentSource, cookieName: string = CONSENT_COOKIE_NAME): ServerConsent | null {\n return readConsent(source, { cookieName });\n}\n\n/** Standard + custom consent categories. */\nexport type ConsentCategory = 'necessary' | 'preferences' | 'statistics' | 'marketing' | (string & {});\n\n/**\n * Whether a category is allowed for this visitor. `necessary` is always allowed. A missing\n * consent (`null` — visitor hasn't decided) is treated as NOT allowed for non-necessary\n * categories (fail-closed / prior-blocking posture), matching the embed.\n */\nexport function isAllowed(consent: ServerConsent | null, category: ConsentCategory): boolean {\n if (category === 'necessary') return true;\n if (!consent) return false;\n // Explicit per-literal access rather than `consent[category]`: the `(string & {})` arm of\n // ConsentCategory defeats control-flow narrowing, so a dynamic index is typed as an\n // implicit-any index into ServerConsent (TS7053, which fails the tsup DTS build).\n if (category === 'preferences') return consent.preferences === true;\n if (category === 'statistics') return consent.statistics === true;\n if (category === 'marketing') return consent.marketing === true;\n return consent.custom?.[category] === true;\n}\n"],"mappings":"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","names":[]}
|
package/dist/consent.cjs
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/consent.ts
|
|
21
|
+
var consent_exports = {};
|
|
22
|
+
__export(consent_exports, {
|
|
23
|
+
CONSENT_COOKIE_NAME: () => CONSENT_COOKIE_NAME,
|
|
24
|
+
getConsent: () => getConsent,
|
|
25
|
+
isAllowed: () => isAllowed,
|
|
26
|
+
parseConsentValue: () => parseConsentValue,
|
|
27
|
+
readConsent: () => readConsent
|
|
28
|
+
});
|
|
29
|
+
module.exports = __toCommonJS(consent_exports);
|
|
30
|
+
var CONSENT_COOKIE_NAME = "CookieMunch";
|
|
31
|
+
function cookieHeaderOf(src) {
|
|
32
|
+
if (typeof src === "string") return src;
|
|
33
|
+
if (!src || typeof src !== "object") return "";
|
|
34
|
+
const h = src.headers;
|
|
35
|
+
if (!h) return "";
|
|
36
|
+
if (typeof h.get === "function") {
|
|
37
|
+
return h.get("cookie") ?? "";
|
|
38
|
+
}
|
|
39
|
+
const c = h.cookie;
|
|
40
|
+
return typeof c === "string" ? c : "";
|
|
41
|
+
}
|
|
42
|
+
function cookieValue(header, name) {
|
|
43
|
+
for (const part of header.split(";")) {
|
|
44
|
+
const eq = part.indexOf("=");
|
|
45
|
+
if (eq === -1) continue;
|
|
46
|
+
if (part.slice(0, eq).trim() === name) return part.slice(eq + 1).trim();
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
function bool(v) {
|
|
51
|
+
return typeof v === "boolean";
|
|
52
|
+
}
|
|
53
|
+
function decode(raw) {
|
|
54
|
+
try {
|
|
55
|
+
return decodeURIComponent(raw);
|
|
56
|
+
} catch {
|
|
57
|
+
return raw;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function normalize(o) {
|
|
61
|
+
if (!o || typeof o !== "object") return null;
|
|
62
|
+
const r = o;
|
|
63
|
+
if (!bool(r.preferences) || !bool(r.statistics) || !bool(r.marketing)) return null;
|
|
64
|
+
if (r.method !== "explicit" && r.method !== "implied") return null;
|
|
65
|
+
if (typeof r.ver !== "number" || typeof r.utc !== "number") return null;
|
|
66
|
+
if (typeof r.stamp !== "string" && typeof r.stamp !== "number") return null;
|
|
67
|
+
if (typeof r.region !== "string") return null;
|
|
68
|
+
const out = {
|
|
69
|
+
necessary: true,
|
|
70
|
+
// never trust a cookie that claims necessary:false
|
|
71
|
+
preferences: r.preferences,
|
|
72
|
+
statistics: r.statistics,
|
|
73
|
+
marketing: r.marketing,
|
|
74
|
+
method: r.method,
|
|
75
|
+
ver: r.ver,
|
|
76
|
+
utc: r.utc,
|
|
77
|
+
region: r.region,
|
|
78
|
+
stamp: String(r.stamp)
|
|
79
|
+
};
|
|
80
|
+
if (r.custom && typeof r.custom === "object" && !Array.isArray(r.custom)) {
|
|
81
|
+
const custom = {};
|
|
82
|
+
for (const [k, v] of Object.entries(r.custom)) if (bool(v)) custom[k] = v;
|
|
83
|
+
if (Object.keys(custom).length > 0) out.custom = custom;
|
|
84
|
+
}
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
function parseConsentValue(raw) {
|
|
88
|
+
if (!raw) return null;
|
|
89
|
+
const decoded = decode(raw);
|
|
90
|
+
try {
|
|
91
|
+
return normalize(JSON.parse(decoded));
|
|
92
|
+
} catch {
|
|
93
|
+
}
|
|
94
|
+
if (!decoded.includes("necessary")) return null;
|
|
95
|
+
const json = decoded.replace(/'/g, '"').replace(/([{,]\s*)([A-Za-z_][\w]*)\s*:/g, '$1"$2":');
|
|
96
|
+
try {
|
|
97
|
+
return normalize(JSON.parse(json));
|
|
98
|
+
} catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
103
|
+
function readConsent(source, opts = {}) {
|
|
104
|
+
const raw = cookieValue(cookieHeaderOf(source), opts.cookieName ?? CONSENT_COOKIE_NAME);
|
|
105
|
+
const consent = raw ? parseConsentValue(raw) : null;
|
|
106
|
+
if (!consent) return null;
|
|
107
|
+
if (typeof opts.minVersion === "number" && consent.ver < opts.minVersion) return null;
|
|
108
|
+
if (typeof opts.expiryDays === "number" && opts.expiryDays > 0) {
|
|
109
|
+
if ((opts.now ?? Date.now()) - consent.utc > opts.expiryDays * DAY_MS) return null;
|
|
110
|
+
}
|
|
111
|
+
return consent;
|
|
112
|
+
}
|
|
113
|
+
function getConsent(source, cookieName = CONSENT_COOKIE_NAME) {
|
|
114
|
+
return readConsent(source, { cookieName });
|
|
115
|
+
}
|
|
116
|
+
function isAllowed(consent, category) {
|
|
117
|
+
if (category === "necessary") return true;
|
|
118
|
+
if (!consent) return false;
|
|
119
|
+
if (category === "preferences") return consent.preferences === true;
|
|
120
|
+
if (category === "statistics") return consent.statistics === true;
|
|
121
|
+
if (category === "marketing") return consent.marketing === true;
|
|
122
|
+
return consent.custom?.[category] === true;
|
|
123
|
+
}
|
|
124
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
125
|
+
0 && (module.exports = {
|
|
126
|
+
CONSENT_COOKIE_NAME,
|
|
127
|
+
getConsent,
|
|
128
|
+
isAllowed,
|
|
129
|
+
parseConsentValue,
|
|
130
|
+
readConsent
|
|
131
|
+
});
|
|
132
|
+
//# sourceMappingURL=consent.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/consent.ts"],"sourcesContent":["/**\n * Server-side consent reading. The Cookie Munch embed stores the visitor's decision in a\n * first-party `CookieMunch` cookie (URL-encoded JSON). This lets a server / edge worker read\n * that decision from the incoming request — to gate server-rendered tags or SSR'd third-party\n * scripts BEFORE they hit the browser — without shipping the whole embed. Zero-dependency and\n * runtime-agnostic: accepts a raw Cookie header, a Node `req` (`req.headers.cookie`), or a\n * Fetch `Request` (`request.headers.get('cookie')`).\n *\n * import { readConsent, isAllowed } from '@cookiemunch/sdk/consent';\n * const consent = readConsent(req.headers.cookie); // ServerConsent | null\n * if (isAllowed(consent, 'statistics')) { …render analytics tag… }\n *\n * The contract is documented at https://cookiemunch.net/docs/consent-cookie. It accepts\n * exactly what the embed accepts, so a server never grants what the browser would refuse:\n * strict JSON (URL-encoded or not) and the relaxed object literal Cookiebot writes.\n *\n * This module imports nothing — `@cookiemunch/sdk/consent` pulls in no API client.\n */\n\n/** The first-party cookie the embed writes (mirrors @cookiemunch/core COOKIE_NAME). */\nexport const CONSENT_COOKIE_NAME = 'CookieMunch';\n\n/** A visitor's consent decision, read from the request cookie. `necessary` is always true. */\nexport interface ServerConsent {\n necessary: true;\n preferences: boolean;\n statistics: boolean;\n marketing: boolean;\n /** How consent was given. `implied` = not yet an explicit choice. */\n method: 'explicit' | 'implied';\n /** Custom (non-standard) categories, when the site defines any. */\n custom?: Record<string, boolean>;\n /** Consent-policy version the decision was made under (see `minVersion`). */\n ver: number;\n /** When the decision was made, epoch milliseconds (see `expiryDays`). */\n utc: number;\n /** Region code the decision was made in (`unknown` when undetected). */\n region: string;\n /** The anonymous consent-receipt id tying this browser to the consent log. */\n stamp: string;\n}\n\n/** The site's consent policy, so the server applies the same rules the embed does. */\nexport interface ReadConsentOptions {\n /** Cookie name. Default `CookieMunch`. */\n cookieName?: string;\n /**\n * The site's current consent version (`consent.version`, or the config `ver` when that is\n * higher). A decision made under an older version is not honoured by the embed — the\n * visitor is asked again — so it reads as `null` here too.\n */\n minVersion?: number;\n /** The site's `consent.expiryDays`: a decision older than this reads as `null`. */\n expiryDays?: number;\n /** Clock for `expiryDays`, epoch ms. Default `Date.now()`. */\n now?: number;\n}\n\n/** Anything we can pull a Cookie header off of. */\nexport type ConsentSource =\n | string\n | null\n | undefined\n | { headers?: { cookie?: string | null; get?(name: string): string | null } | Record<string, unknown> };\n\nfunction cookieHeaderOf(src: ConsentSource): string {\n if (typeof src === 'string') return src;\n if (!src || typeof src !== 'object') return '';\n const h = src.headers;\n if (!h) return '';\n // Fetch Request: headers.get('cookie'); Node: headers.cookie.\n if (typeof (h as { get?(n: string): string | null }).get === 'function') {\n return (h as { get(n: string): string | null }).get('cookie') ?? '';\n }\n const c = (h as Record<string, unknown>).cookie;\n return typeof c === 'string' ? c : '';\n}\n\n/** Extract one cookie's raw value from a Cookie header (no decoding). */\nfunction cookieValue(header: string, name: string): string | null {\n for (const part of header.split(';')) {\n const eq = part.indexOf('=');\n if (eq === -1) continue;\n if (part.slice(0, eq).trim() === name) return part.slice(eq + 1).trim();\n }\n return null;\n}\n\nfunction bool(v: unknown): v is boolean {\n return typeof v === 'boolean';\n}\n\nfunction decode(raw: string): string {\n try {\n return decodeURIComponent(raw);\n } catch {\n return raw; // not URL-encoded; use as-is\n }\n}\n\n/** The embed's own validation (core cookie.ts `normalize`), field for field. */\nfunction normalize(o: unknown): ServerConsent | null {\n if (!o || typeof o !== 'object') return null;\n const r = o as Record<string, unknown>;\n if (!bool(r.preferences) || !bool(r.statistics) || !bool(r.marketing)) return null;\n if (r.method !== 'explicit' && r.method !== 'implied') return null;\n if (typeof r.ver !== 'number' || typeof r.utc !== 'number') return null;\n if (typeof r.stamp !== 'string' && typeof r.stamp !== 'number') return null;\n if (typeof r.region !== 'string') return null;\n const out: ServerConsent = {\n necessary: true, // never trust a cookie that claims necessary:false\n preferences: r.preferences,\n statistics: r.statistics,\n marketing: r.marketing,\n method: r.method,\n ver: r.ver,\n utc: r.utc,\n region: r.region,\n stamp: String(r.stamp),\n };\n if (r.custom && typeof r.custom === 'object' && !Array.isArray(r.custom)) {\n const custom: Record<string, boolean> = {};\n for (const [k, v] of Object.entries(r.custom as Record<string, unknown>)) if (bool(v)) custom[k] = v;\n if (Object.keys(custom).length > 0) out.custom = custom;\n }\n return out;\n}\n\n/**\n * Parse a raw cookie value into a validated ServerConsent, or null. Accepts the two formats\n * the embed reads: URL-encoded (or plain) strict JSON, and Cookiebot's relaxed object\n * literal — `{stamp:'…',necessary:true,…}` — left behind by a site migrating from Cookiebot.\n */\nexport function parseConsentValue(raw: string): ServerConsent | null {\n if (!raw) return null;\n const decoded = decode(raw);\n try {\n return normalize(JSON.parse(decoded));\n } catch {\n /* not strict JSON — try the Cookiebot literal */\n }\n if (!decoded.includes('necessary')) return null;\n const json = decoded.replace(/'/g, '\"').replace(/([{,]\\s*)([A-Za-z_][\\w]*)\\s*:/g, '$1\"$2\":');\n try {\n return normalize(JSON.parse(json));\n } catch {\n return null;\n }\n}\n\nconst DAY_MS = 24 * 60 * 60 * 1000;\n\n/**\n * Read the visitor's consent from a request: a raw `Cookie` header, a Node `req`, or a Fetch\n * `Request`. Returns null when there is no decision the embed would honour — no cookie, an\n * unparseable one, or (with the options) one made under an older version or past expiry.\n */\nexport function readConsent(source: ConsentSource, opts: ReadConsentOptions = {}): ServerConsent | null {\n const raw = cookieValue(cookieHeaderOf(source), opts.cookieName ?? CONSENT_COOKIE_NAME);\n const consent = raw ? parseConsentValue(raw) : null;\n if (!consent) return null;\n if (typeof opts.minVersion === 'number' && consent.ver < opts.minVersion) return null;\n if (typeof opts.expiryDays === 'number' && opts.expiryDays > 0) {\n if ((opts.now ?? Date.now()) - consent.utc > opts.expiryDays * DAY_MS) return null;\n }\n return consent;\n}\n\n/**\n * Read the visitor's consent from a request. The original spelling of {@link readConsent},\n * kept for existing callers; the second argument is the cookie name.\n */\nexport function getConsent(source: ConsentSource, cookieName: string = CONSENT_COOKIE_NAME): ServerConsent | null {\n return readConsent(source, { cookieName });\n}\n\n/** Standard + custom consent categories. */\nexport type ConsentCategory = 'necessary' | 'preferences' | 'statistics' | 'marketing' | (string & {});\n\n/**\n * Whether a category is allowed for this visitor. `necessary` is always allowed. A missing\n * consent (`null` — visitor hasn't decided) is treated as NOT allowed for non-necessary\n * categories (fail-closed / prior-blocking posture), matching the embed.\n */\nexport function isAllowed(consent: ServerConsent | null, category: ConsentCategory): boolean {\n if (category === 'necessary') return true;\n if (!consent) return false;\n // Explicit per-literal access rather than `consent[category]`: the `(string & {})` arm of\n // ConsentCategory defeats control-flow narrowing, so a dynamic index is typed as an\n // implicit-any index into ServerConsent (TS7053, which fails the tsup DTS build).\n if (category === 'preferences') return consent.preferences === true;\n if (category === 'statistics') return consent.statistics === true;\n if (category === 'marketing') return consent.marketing === true;\n return consent.custom?.[category] === true;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAoBO,IAAM,sBAAsB;AA6CnC,SAAS,eAAe,KAA4B;AAClD,MAAI,OAAO,QAAQ,SAAU,QAAO;AACpC,MAAI,CAAC,OAAO,OAAO,QAAQ,SAAU,QAAO;AAC5C,QAAM,IAAI,IAAI;AACd,MAAI,CAAC,EAAG,QAAO;AAEf,MAAI,OAAQ,EAAyC,QAAQ,YAAY;AACvE,WAAQ,EAAwC,IAAI,QAAQ,KAAK;AAAA,EACnE;AACA,QAAM,IAAK,EAA8B;AACzC,SAAO,OAAO,MAAM,WAAW,IAAI;AACrC;AAGA,SAAS,YAAY,QAAgB,MAA6B;AAChE,aAAW,QAAQ,OAAO,MAAM,GAAG,GAAG;AACpC,UAAM,KAAK,KAAK,QAAQ,GAAG;AAC3B,QAAI,OAAO,GAAI;AACf,QAAI,KAAK,MAAM,GAAG,EAAE,EAAE,KAAK,MAAM,KAAM,QAAO,KAAK,MAAM,KAAK,CAAC,EAAE,KAAK;AAAA,EACxE;AACA,SAAO;AACT;AAEA,SAAS,KAAK,GAA0B;AACtC,SAAO,OAAO,MAAM;AACtB;AAEA,SAAS,OAAO,KAAqB;AACnC,MAAI;AACF,WAAO,mBAAmB,GAAG;AAAA,EAC/B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGA,SAAS,UAAU,GAAkC;AACnD,MAAI,CAAC,KAAK,OAAO,MAAM,SAAU,QAAO;AACxC,QAAM,IAAI;AACV,MAAI,CAAC,KAAK,EAAE,WAAW,KAAK,CAAC,KAAK,EAAE,UAAU,KAAK,CAAC,KAAK,EAAE,SAAS,EAAG,QAAO;AAC9E,MAAI,EAAE,WAAW,cAAc,EAAE,WAAW,UAAW,QAAO;AAC9D,MAAI,OAAO,EAAE,QAAQ,YAAY,OAAO,EAAE,QAAQ,SAAU,QAAO;AACnE,MAAI,OAAO,EAAE,UAAU,YAAY,OAAO,EAAE,UAAU,SAAU,QAAO;AACvE,MAAI,OAAO,EAAE,WAAW,SAAU,QAAO;AACzC,QAAM,MAAqB;AAAA,IACzB,WAAW;AAAA;AAAA,IACX,aAAa,EAAE;AAAA,IACf,YAAY,EAAE;AAAA,IACd,WAAW,EAAE;AAAA,IACb,QAAQ,EAAE;AAAA,IACV,KAAK,EAAE;AAAA,IACP,KAAK,EAAE;AAAA,IACP,QAAQ,EAAE;AAAA,IACV,OAAO,OAAO,EAAE,KAAK;AAAA,EACvB;AACA,MAAI,EAAE,UAAU,OAAO,EAAE,WAAW,YAAY,CAAC,MAAM,QAAQ,EAAE,MAAM,GAAG;AACxE,UAAM,SAAkC,CAAC;AACzC,eAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,EAAE,MAAiC,EAAG,KAAI,KAAK,CAAC,EAAG,QAAO,CAAC,IAAI;AACnG,QAAI,OAAO,KAAK,MAAM,EAAE,SAAS,EAAG,KAAI,SAAS;AAAA,EACnD;AACA,SAAO;AACT;AAOO,SAAS,kBAAkB,KAAmC;AACnE,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,UAAU,OAAO,GAAG;AAC1B,MAAI;AACF,WAAO,UAAU,KAAK,MAAM,OAAO,CAAC;AAAA,EACtC,QAAQ;AAAA,EAER;AACA,MAAI,CAAC,QAAQ,SAAS,WAAW,EAAG,QAAO;AAC3C,QAAM,OAAO,QAAQ,QAAQ,MAAM,GAAG,EAAE,QAAQ,kCAAkC,SAAS;AAC3F,MAAI;AACF,WAAO,UAAU,KAAK,MAAM,IAAI,CAAC;AAAA,EACnC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,IAAM,SAAS,KAAK,KAAK,KAAK;AAOvB,SAAS,YAAY,QAAuB,OAA2B,CAAC,GAAyB;AACtG,QAAM,MAAM,YAAY,eAAe,MAAM,GAAG,KAAK,cAAc,mBAAmB;AACtF,QAAM,UAAU,MAAM,kBAAkB,GAAG,IAAI;AAC/C,MAAI,CAAC,QAAS,QAAO;AACrB,MAAI,OAAO,KAAK,eAAe,YAAY,QAAQ,MAAM,KAAK,WAAY,QAAO;AACjF,MAAI,OAAO,KAAK,eAAe,YAAY,KAAK,aAAa,GAAG;AAC9D,SAAK,KAAK,OAAO,KAAK,IAAI,KAAK,QAAQ,MAAM,KAAK,aAAa,OAAQ,QAAO;AAAA,EAChF;AACA,SAAO;AACT;AAMO,SAAS,WAAW,QAAuB,aAAqB,qBAA2C;AAChH,SAAO,YAAY,QAAQ,EAAE,WAAW,CAAC;AAC3C;AAUO,SAAS,UAAU,SAA+B,UAAoC;AAC3F,MAAI,aAAa,YAAa,QAAO;AACrC,MAAI,CAAC,QAAS,QAAO;AAIrB,MAAI,aAAa,cAAe,QAAO,QAAQ,gBAAgB;AAC/D,MAAI,aAAa,aAAc,QAAO,QAAQ,eAAe;AAC7D,MAAI,aAAa,YAAa,QAAO,QAAQ,cAAc;AAC3D,SAAO,QAAQ,SAAS,QAAQ,MAAM;AACxC;","names":[]}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side consent reading. The Cookie Munch embed stores the visitor's decision in a
|
|
3
|
+
* first-party `CookieMunch` cookie (URL-encoded JSON). This lets a server / edge worker read
|
|
4
|
+
* that decision from the incoming request — to gate server-rendered tags or SSR'd third-party
|
|
5
|
+
* scripts BEFORE they hit the browser — without shipping the whole embed. Zero-dependency and
|
|
6
|
+
* runtime-agnostic: accepts a raw Cookie header, a Node `req` (`req.headers.cookie`), or a
|
|
7
|
+
* Fetch `Request` (`request.headers.get('cookie')`).
|
|
8
|
+
*
|
|
9
|
+
* import { readConsent, isAllowed } from '@cookiemunch/sdk/consent';
|
|
10
|
+
* const consent = readConsent(req.headers.cookie); // ServerConsent | null
|
|
11
|
+
* if (isAllowed(consent, 'statistics')) { …render analytics tag… }
|
|
12
|
+
*
|
|
13
|
+
* The contract is documented at https://cookiemunch.net/docs/consent-cookie. It accepts
|
|
14
|
+
* exactly what the embed accepts, so a server never grants what the browser would refuse:
|
|
15
|
+
* strict JSON (URL-encoded or not) and the relaxed object literal Cookiebot writes.
|
|
16
|
+
*
|
|
17
|
+
* This module imports nothing — `@cookiemunch/sdk/consent` pulls in no API client.
|
|
18
|
+
*/
|
|
19
|
+
/** The first-party cookie the embed writes (mirrors @cookiemunch/core COOKIE_NAME). */
|
|
20
|
+
declare const CONSENT_COOKIE_NAME = "CookieMunch";
|
|
21
|
+
/** A visitor's consent decision, read from the request cookie. `necessary` is always true. */
|
|
22
|
+
interface ServerConsent {
|
|
23
|
+
necessary: true;
|
|
24
|
+
preferences: boolean;
|
|
25
|
+
statistics: boolean;
|
|
26
|
+
marketing: boolean;
|
|
27
|
+
/** How consent was given. `implied` = not yet an explicit choice. */
|
|
28
|
+
method: 'explicit' | 'implied';
|
|
29
|
+
/** Custom (non-standard) categories, when the site defines any. */
|
|
30
|
+
custom?: Record<string, boolean>;
|
|
31
|
+
/** Consent-policy version the decision was made under (see `minVersion`). */
|
|
32
|
+
ver: number;
|
|
33
|
+
/** When the decision was made, epoch milliseconds (see `expiryDays`). */
|
|
34
|
+
utc: number;
|
|
35
|
+
/** Region code the decision was made in (`unknown` when undetected). */
|
|
36
|
+
region: string;
|
|
37
|
+
/** The anonymous consent-receipt id tying this browser to the consent log. */
|
|
38
|
+
stamp: string;
|
|
39
|
+
}
|
|
40
|
+
/** The site's consent policy, so the server applies the same rules the embed does. */
|
|
41
|
+
interface ReadConsentOptions {
|
|
42
|
+
/** Cookie name. Default `CookieMunch`. */
|
|
43
|
+
cookieName?: string;
|
|
44
|
+
/**
|
|
45
|
+
* The site's current consent version (`consent.version`, or the config `ver` when that is
|
|
46
|
+
* higher). A decision made under an older version is not honoured by the embed — the
|
|
47
|
+
* visitor is asked again — so it reads as `null` here too.
|
|
48
|
+
*/
|
|
49
|
+
minVersion?: number;
|
|
50
|
+
/** The site's `consent.expiryDays`: a decision older than this reads as `null`. */
|
|
51
|
+
expiryDays?: number;
|
|
52
|
+
/** Clock for `expiryDays`, epoch ms. Default `Date.now()`. */
|
|
53
|
+
now?: number;
|
|
54
|
+
}
|
|
55
|
+
/** Anything we can pull a Cookie header off of. */
|
|
56
|
+
type ConsentSource = string | null | undefined | {
|
|
57
|
+
headers?: {
|
|
58
|
+
cookie?: string | null;
|
|
59
|
+
get?(name: string): string | null;
|
|
60
|
+
} | Record<string, unknown>;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Parse a raw cookie value into a validated ServerConsent, or null. Accepts the two formats
|
|
64
|
+
* the embed reads: URL-encoded (or plain) strict JSON, and Cookiebot's relaxed object
|
|
65
|
+
* literal — `{stamp:'…',necessary:true,…}` — left behind by a site migrating from Cookiebot.
|
|
66
|
+
*/
|
|
67
|
+
declare function parseConsentValue(raw: string): ServerConsent | null;
|
|
68
|
+
/**
|
|
69
|
+
* Read the visitor's consent from a request: a raw `Cookie` header, a Node `req`, or a Fetch
|
|
70
|
+
* `Request`. Returns null when there is no decision the embed would honour — no cookie, an
|
|
71
|
+
* unparseable one, or (with the options) one made under an older version or past expiry.
|
|
72
|
+
*/
|
|
73
|
+
declare function readConsent(source: ConsentSource, opts?: ReadConsentOptions): ServerConsent | null;
|
|
74
|
+
/**
|
|
75
|
+
* Read the visitor's consent from a request. The original spelling of {@link readConsent},
|
|
76
|
+
* kept for existing callers; the second argument is the cookie name.
|
|
77
|
+
*/
|
|
78
|
+
declare function getConsent(source: ConsentSource, cookieName?: string): ServerConsent | null;
|
|
79
|
+
/** Standard + custom consent categories. */
|
|
80
|
+
type ConsentCategory = 'necessary' | 'preferences' | 'statistics' | 'marketing' | (string & {});
|
|
81
|
+
/**
|
|
82
|
+
* Whether a category is allowed for this visitor. `necessary` is always allowed. A missing
|
|
83
|
+
* consent (`null` — visitor hasn't decided) is treated as NOT allowed for non-necessary
|
|
84
|
+
* categories (fail-closed / prior-blocking posture), matching the embed.
|
|
85
|
+
*/
|
|
86
|
+
declare function isAllowed(consent: ServerConsent | null, category: ConsentCategory): boolean;
|
|
87
|
+
|
|
88
|
+
export { CONSENT_COOKIE_NAME, type ConsentCategory, type ConsentSource, type ReadConsentOptions, type ServerConsent, getConsent, isAllowed, parseConsentValue, readConsent };
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side consent reading. The Cookie Munch embed stores the visitor's decision in a
|
|
3
|
+
* first-party `CookieMunch` cookie (URL-encoded JSON). This lets a server / edge worker read
|
|
4
|
+
* that decision from the incoming request — to gate server-rendered tags or SSR'd third-party
|
|
5
|
+
* scripts BEFORE they hit the browser — without shipping the whole embed. Zero-dependency and
|
|
6
|
+
* runtime-agnostic: accepts a raw Cookie header, a Node `req` (`req.headers.cookie`), or a
|
|
7
|
+
* Fetch `Request` (`request.headers.get('cookie')`).
|
|
8
|
+
*
|
|
9
|
+
* import { readConsent, isAllowed } from '@cookiemunch/sdk/consent';
|
|
10
|
+
* const consent = readConsent(req.headers.cookie); // ServerConsent | null
|
|
11
|
+
* if (isAllowed(consent, 'statistics')) { …render analytics tag… }
|
|
12
|
+
*
|
|
13
|
+
* The contract is documented at https://cookiemunch.net/docs/consent-cookie. It accepts
|
|
14
|
+
* exactly what the embed accepts, so a server never grants what the browser would refuse:
|
|
15
|
+
* strict JSON (URL-encoded or not) and the relaxed object literal Cookiebot writes.
|
|
16
|
+
*
|
|
17
|
+
* This module imports nothing — `@cookiemunch/sdk/consent` pulls in no API client.
|
|
18
|
+
*/
|
|
19
|
+
/** The first-party cookie the embed writes (mirrors @cookiemunch/core COOKIE_NAME). */
|
|
20
|
+
declare const CONSENT_COOKIE_NAME = "CookieMunch";
|
|
21
|
+
/** A visitor's consent decision, read from the request cookie. `necessary` is always true. */
|
|
22
|
+
interface ServerConsent {
|
|
23
|
+
necessary: true;
|
|
24
|
+
preferences: boolean;
|
|
25
|
+
statistics: boolean;
|
|
26
|
+
marketing: boolean;
|
|
27
|
+
/** How consent was given. `implied` = not yet an explicit choice. */
|
|
28
|
+
method: 'explicit' | 'implied';
|
|
29
|
+
/** Custom (non-standard) categories, when the site defines any. */
|
|
30
|
+
custom?: Record<string, boolean>;
|
|
31
|
+
/** Consent-policy version the decision was made under (see `minVersion`). */
|
|
32
|
+
ver: number;
|
|
33
|
+
/** When the decision was made, epoch milliseconds (see `expiryDays`). */
|
|
34
|
+
utc: number;
|
|
35
|
+
/** Region code the decision was made in (`unknown` when undetected). */
|
|
36
|
+
region: string;
|
|
37
|
+
/** The anonymous consent-receipt id tying this browser to the consent log. */
|
|
38
|
+
stamp: string;
|
|
39
|
+
}
|
|
40
|
+
/** The site's consent policy, so the server applies the same rules the embed does. */
|
|
41
|
+
interface ReadConsentOptions {
|
|
42
|
+
/** Cookie name. Default `CookieMunch`. */
|
|
43
|
+
cookieName?: string;
|
|
44
|
+
/**
|
|
45
|
+
* The site's current consent version (`consent.version`, or the config `ver` when that is
|
|
46
|
+
* higher). A decision made under an older version is not honoured by the embed — the
|
|
47
|
+
* visitor is asked again — so it reads as `null` here too.
|
|
48
|
+
*/
|
|
49
|
+
minVersion?: number;
|
|
50
|
+
/** The site's `consent.expiryDays`: a decision older than this reads as `null`. */
|
|
51
|
+
expiryDays?: number;
|
|
52
|
+
/** Clock for `expiryDays`, epoch ms. Default `Date.now()`. */
|
|
53
|
+
now?: number;
|
|
54
|
+
}
|
|
55
|
+
/** Anything we can pull a Cookie header off of. */
|
|
56
|
+
type ConsentSource = string | null | undefined | {
|
|
57
|
+
headers?: {
|
|
58
|
+
cookie?: string | null;
|
|
59
|
+
get?(name: string): string | null;
|
|
60
|
+
} | Record<string, unknown>;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Parse a raw cookie value into a validated ServerConsent, or null. Accepts the two formats
|
|
64
|
+
* the embed reads: URL-encoded (or plain) strict JSON, and Cookiebot's relaxed object
|
|
65
|
+
* literal — `{stamp:'…',necessary:true,…}` — left behind by a site migrating from Cookiebot.
|
|
66
|
+
*/
|
|
67
|
+
declare function parseConsentValue(raw: string): ServerConsent | null;
|
|
68
|
+
/**
|
|
69
|
+
* Read the visitor's consent from a request: a raw `Cookie` header, a Node `req`, or a Fetch
|
|
70
|
+
* `Request`. Returns null when there is no decision the embed would honour — no cookie, an
|
|
71
|
+
* unparseable one, or (with the options) one made under an older version or past expiry.
|
|
72
|
+
*/
|
|
73
|
+
declare function readConsent(source: ConsentSource, opts?: ReadConsentOptions): ServerConsent | null;
|
|
74
|
+
/**
|
|
75
|
+
* Read the visitor's consent from a request. The original spelling of {@link readConsent},
|
|
76
|
+
* kept for existing callers; the second argument is the cookie name.
|
|
77
|
+
*/
|
|
78
|
+
declare function getConsent(source: ConsentSource, cookieName?: string): ServerConsent | null;
|
|
79
|
+
/** Standard + custom consent categories. */
|
|
80
|
+
type ConsentCategory = 'necessary' | 'preferences' | 'statistics' | 'marketing' | (string & {});
|
|
81
|
+
/**
|
|
82
|
+
* Whether a category is allowed for this visitor. `necessary` is always allowed. A missing
|
|
83
|
+
* consent (`null` — visitor hasn't decided) is treated as NOT allowed for non-necessary
|
|
84
|
+
* categories (fail-closed / prior-blocking posture), matching the embed.
|
|
85
|
+
*/
|
|
86
|
+
declare function isAllowed(consent: ServerConsent | null, category: ConsentCategory): boolean;
|
|
87
|
+
|
|
88
|
+
export { CONSENT_COOKIE_NAME, type ConsentCategory, type ConsentSource, type ReadConsentOptions, type ServerConsent, getConsent, isAllowed, parseConsentValue, readConsent };
|
package/dist/consent.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CONSENT_COOKIE_NAME,
|
|
3
|
+
getConsent,
|
|
4
|
+
isAllowed,
|
|
5
|
+
parseConsentValue,
|
|
6
|
+
readConsent
|
|
7
|
+
} from "./chunk-G7ORTXTK.js";
|
|
8
|
+
export {
|
|
9
|
+
CONSENT_COOKIE_NAME,
|
|
10
|
+
getConsent,
|
|
11
|
+
isAllowed,
|
|
12
|
+
parseConsentValue,
|
|
13
|
+
readConsent
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=consent.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|