@cookiemunch/sdk 0.5.1 → 0.6.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cookiemunch/sdk",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "Typed REST client for the Cookie Munch Developer API (/v1)",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,104 +0,0 @@
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
@@ -1 +0,0 @@
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":[]}