@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 CHANGED
@@ -1,3 +1,60 @@
1
- # Temporary Holding Version
1
+ # @cookiemunch/sdk
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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":[]}
@@ -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":"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","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 };
@@ -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":[]}