@cookiemunch/sdk 0.5.1 → 0.7.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/CHANGELOG.md +22 -0
- package/dist/chunk-OSVTQPQG.js +187 -0
- package/dist/chunk-OSVTQPQG.js.map +1 -0
- package/dist/consent.cjs +89 -2
- package/dist/consent.cjs.map +1 -1
- package/dist/consent.d.cts +83 -2
- package/dist/consent.d.ts +83 -2
- package/dist/consent.js +11 -3
- package/dist/index.cjs +415 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1268 -41
- package/dist/index.d.ts +1268 -41
- package/dist/index.js +332 -16
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-G7ORTXTK.js +0 -104
- package/dist/chunk-G7ORTXTK.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
Every published release, newest first. Dates are the day the version was cut.
|
|
4
4
|
|
|
5
|
+
## 0.7.0 (2026-10-24)
|
|
6
|
+
|
|
7
|
+
- Added: `sandbox.get` / `create` / `delete` — a sandbox organisation made from yours: the same people and plan, never billed, test keys only, every rights request a test.
|
|
8
|
+
- Added: dated API versions — the `apiVersion` client option (sends `CookieMunch-Version`), `versions()`, and `keys.updateSelf({ apiVersion })`. Keys minted from 2026-10-24 are strict about unknown body fields by default; older keys behave as before.
|
|
9
|
+
- Added: `exchangeOperatorToken()` — swap an identity provider's token for a key that lasts at most an hour (RFC 8693 token exchange).
|
|
10
|
+
- Added: `sandbox` and `sandboxOf` on `me()`.
|
|
11
|
+
|
|
12
|
+
## 0.6.0 (2026-10-24)
|
|
13
|
+
|
|
14
|
+
- Added: `dsar.openCase` / `getCase` / `cancelCase` / `caseReceipt` / `caseReceiptPdf` / `listByTrace` — rights cases fanned out to every connected system, with one `executeAfter`, a trace and a signed receipt.
|
|
15
|
+
- Added: `dsarSettings.get` / `set` (a site's `graceDays`). **Opt-in for existing sites:** a site that existed before grace periods shipped reads `graceDays: 0` (`source: 'legacy'`) and its erasures run exactly as before until you set one; new sites start at 7.
|
|
16
|
+
- Added: `fulfillment.executorConformance` / `rollExecutorCompletionSecret`, `integrations.subjectCard`, and `runExecutorConformance()` — the cm-standard-v2 conformance kit as a function, no Cookie Munch key needed.
|
|
17
|
+
- Added: `closure` and `restriction` types, the `erasure`/`erase`/`export` aliases, and the `cancelled` status.
|
|
18
|
+
- Added: `fulfillment.executor(id)` — one connection with its full profile. Read it before changing `profile`: `updateExecutor` replaces the whole profile.
|
|
19
|
+
- Added: keys — `keys.self()`, `keys.rollSelf()`, `keys.vend({ scopes, ttlSeconds })` (short-lived, at most 12 h), `keys.listStandard()`, `keys.revokeLeaked(report)`; keys can be minted with `scopes`, `cbids` and `expiresInDays`.
|
|
20
|
+
- Added: `partnerLinks.list` / `create` / `revoke` (any partner, scoped), and `metadata.get` / `update` with `metadata` and `externalRef` on sites, webhooks, connections, rights requests and orgs; `?metadata[key]=` list filters; `deleteSandbox` on sandbox sites and orgs.
|
|
21
|
+
- Added: `sites.cookieSuggestions` / `declareCookieSuggestions`, `importCookieManifest` (any vendor's embed manifest) and `removeCookiePreset` for custom presets; `embedManifest(cbid)`; first-class cookie presets for `referalli`, `firstpromoter`, `partnerstack`, `tapfiliate` and `impact` (`referally` is a deprecated alias).
|
|
22
|
+
- Added: `sites.getBrand` / `setBrand` (DTCG brand.json import with a read-back), `consent.insights`, `categoryRegistry()`, `entitlements()`, `provisionEnv(ref)`, and `provision(input, { fromStep })` with the forgedeck (v2) body.
|
|
23
|
+
- Added: `healthWaivers.list` / `set` / `remove`, `auditSink.get` / `set` / `remove` (CloudEvents), `webhooks.replay(id, since)`, `audit(limit, { actor: 'self' })`, `fulfillment.recheckExecutor` and `executorHealthHistory`.
|
|
24
|
+
- Added: `readConsent(source, { requireSigned })`, `verifyDecisionSignature`, `verifyToken` (consent tokens, against the partner JWKS) and `consentSignaturePayload`.
|
|
25
|
+
- Added: the `X-Secret-Recipient` option seals every once-shown secret; `Idempotency-Key` on creates.
|
|
26
|
+
|
|
5
27
|
## 0.5.1 (2026-10-23)
|
|
6
28
|
|
|
7
29
|
- Fixed: `sites.waitForScan` no longer sends one extra poll in the last few milliseconds before its deadline when the server answers at once.
|
|
@@ -0,0 +1,187 @@
|
|
|
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
|
+
if (typeof r.sig === "string") out.sig = r.sig;
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
function parseConsentValue(raw) {
|
|
61
|
+
if (!raw) return null;
|
|
62
|
+
const decoded = decode(raw);
|
|
63
|
+
try {
|
|
64
|
+
return normalize(JSON.parse(decoded));
|
|
65
|
+
} catch {
|
|
66
|
+
}
|
|
67
|
+
if (!decoded.includes("necessary")) return null;
|
|
68
|
+
const json = decoded.replace(/'/g, '"').replace(/([{,]\s*)([A-Za-z_][\w]*)\s*:/g, '$1"$2":');
|
|
69
|
+
try {
|
|
70
|
+
return normalize(JSON.parse(json));
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
76
|
+
function readConsent(source, opts = {}) {
|
|
77
|
+
const consent = readUnsigned(source, opts);
|
|
78
|
+
if (opts.requireSigned !== true) return consent;
|
|
79
|
+
if (!consent?.sig) return Promise.resolve(null);
|
|
80
|
+
return verifyDecisionSignature(consent, opts).then((ok) => ok ? consent : null);
|
|
81
|
+
}
|
|
82
|
+
function readUnsigned(source, opts) {
|
|
83
|
+
const raw = cookieValue(cookieHeaderOf(source), opts.cookieName ?? CONSENT_COOKIE_NAME);
|
|
84
|
+
const consent = raw ? parseConsentValue(raw) : null;
|
|
85
|
+
if (!consent) return null;
|
|
86
|
+
if (typeof opts.minVersion === "number" && consent.ver < opts.minVersion) return null;
|
|
87
|
+
if (typeof opts.expiryDays === "number" && opts.expiryDays > 0) {
|
|
88
|
+
if ((opts.now ?? Date.now()) - consent.utc > opts.expiryDays * DAY_MS) return null;
|
|
89
|
+
}
|
|
90
|
+
return consent;
|
|
91
|
+
}
|
|
92
|
+
function getConsent(source, cookieName = CONSENT_COOKIE_NAME) {
|
|
93
|
+
return readConsent(source, { cookieName });
|
|
94
|
+
}
|
|
95
|
+
function isAllowed(consent, category) {
|
|
96
|
+
if (category === "necessary") return true;
|
|
97
|
+
if (!consent) return false;
|
|
98
|
+
if (category === "preferences") return consent.preferences === true;
|
|
99
|
+
if (category === "statistics") return consent.statistics === true;
|
|
100
|
+
if (category === "marketing") return consent.marketing === true;
|
|
101
|
+
return consent.custom?.[category] === true;
|
|
102
|
+
}
|
|
103
|
+
var DEFAULT_JWKS_URL = "https://api.cookiemunch.net/v1/partner/jwks.json";
|
|
104
|
+
function consentSignaturePayload(d) {
|
|
105
|
+
const b = (v) => v ? "1" : "0";
|
|
106
|
+
const custom = d.custom ?? {};
|
|
107
|
+
const named = Object.keys(custom).sort().map((k) => `${k}=${b(custom[k])}`).join(",");
|
|
108
|
+
return `cmc1|${d.stamp}|${d.ver}|${d.utc}|${b(d.preferences)}${b(d.statistics)}${b(d.marketing)}|${named}`;
|
|
109
|
+
}
|
|
110
|
+
var jwksCache = /* @__PURE__ */ new Map();
|
|
111
|
+
var JWKS_TTL_MS = 60 * 60 * 1e3;
|
|
112
|
+
async function keySet(opts, kid) {
|
|
113
|
+
if (opts.jwks) return opts.jwks;
|
|
114
|
+
const url = opts.jwksUrl ?? DEFAULT_JWKS_URL;
|
|
115
|
+
const hit = jwksCache.get(url);
|
|
116
|
+
if (hit && Date.now() - hit.at < JWKS_TTL_MS && hit.set.keys.some((k) => k.kid === kid)) return hit.set;
|
|
117
|
+
const f = opts.fetch ?? globalThis.fetch;
|
|
118
|
+
if (!f) throw new Error("no fetch available to load the key set; pass `jwks`");
|
|
119
|
+
const res = await f(url);
|
|
120
|
+
if (!res.ok) throw new Error(`key set ${url} answered ${res.status}`);
|
|
121
|
+
const set = await res.json();
|
|
122
|
+
if (!set || !Array.isArray(set.keys)) throw new Error(`key set ${url} is not a JWKS`);
|
|
123
|
+
jwksCache.set(url, { at: Date.now(), set });
|
|
124
|
+
return set;
|
|
125
|
+
}
|
|
126
|
+
function b64urlBytes(s) {
|
|
127
|
+
const b64 = s.replace(/-/g, "+").replace(/_/g, "/") + "===".slice((s.length + 3) % 4);
|
|
128
|
+
const bin = atob(b64);
|
|
129
|
+
const out = new Uint8Array(bin.length);
|
|
130
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
131
|
+
return out;
|
|
132
|
+
}
|
|
133
|
+
async function edVerify(jwk, data, signature) {
|
|
134
|
+
const subtle = globalThis.crypto?.subtle;
|
|
135
|
+
if (!subtle) throw new Error("WebCrypto is unavailable in this runtime");
|
|
136
|
+
if (jwk.kty !== "OKP" || jwk.crv !== "Ed25519" || !jwk.x) return false;
|
|
137
|
+
const key = await subtle.importKey("jwk", { kty: "OKP", crv: "Ed25519", x: jwk.x }, { name: "Ed25519" }, false, ["verify"]);
|
|
138
|
+
return subtle.verify({ name: "Ed25519" }, key, b64urlBytes(signature), new TextEncoder().encode(data));
|
|
139
|
+
}
|
|
140
|
+
async function verifyDecisionSignature(consent, opts = {}) {
|
|
141
|
+
const dot = consent.sig?.lastIndexOf(".") ?? -1;
|
|
142
|
+
if (!consent.sig || dot <= 0) return false;
|
|
143
|
+
const kid = consent.sig.slice(0, dot);
|
|
144
|
+
try {
|
|
145
|
+
const jwk = (await keySet(opts, kid)).keys.find((k) => k.kid === kid);
|
|
146
|
+
if (!jwk) return false;
|
|
147
|
+
return await edVerify(jwk, consentSignaturePayload(consent), consent.sig.slice(dot + 1));
|
|
148
|
+
} catch {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
async function verifyToken(token, opts = {}) {
|
|
153
|
+
const parts = typeof token === "string" ? token.split(".") : [];
|
|
154
|
+
if (parts.length !== 3) throw new Error("not a JWT");
|
|
155
|
+
const [h, p, s] = parts;
|
|
156
|
+
let header;
|
|
157
|
+
let claims;
|
|
158
|
+
try {
|
|
159
|
+
header = JSON.parse(new TextDecoder().decode(b64urlBytes(h)));
|
|
160
|
+
claims = JSON.parse(new TextDecoder().decode(b64urlBytes(p)));
|
|
161
|
+
} catch {
|
|
162
|
+
throw new Error("malformed token");
|
|
163
|
+
}
|
|
164
|
+
if (header.alg !== "EdDSA" || !header.kid) throw new Error("unexpected token algorithm");
|
|
165
|
+
const jwk = (await keySet(opts, header.kid)).keys.find((k) => k.kid === header.kid);
|
|
166
|
+
if (!jwk || !await edVerify(jwk, `${h}.${p}`, s)) throw new Error("bad signature");
|
|
167
|
+
if (claims.token_use !== "consent") throw new Error("not a consent token");
|
|
168
|
+
const now = (opts.now ?? Date.now()) / 1e3;
|
|
169
|
+
const leeway = opts.leewaySeconds ?? 30;
|
|
170
|
+
if (typeof claims.exp !== "number" || now > claims.exp + leeway) throw new Error("expired");
|
|
171
|
+
if (typeof claims.iat === "number" && claims.iat > now + leeway) throw new Error("issued in the future");
|
|
172
|
+
if (opts.cbid !== void 0 && (claims.cbid !== opts.cbid || claims.aud !== `cookiemunch:${opts.cbid}`)) throw new Error("token is for another site");
|
|
173
|
+
return claims;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export {
|
|
177
|
+
CONSENT_COOKIE_NAME,
|
|
178
|
+
parseConsentValue,
|
|
179
|
+
readConsent,
|
|
180
|
+
getConsent,
|
|
181
|
+
isAllowed,
|
|
182
|
+
DEFAULT_JWKS_URL,
|
|
183
|
+
consentSignaturePayload,
|
|
184
|
+
verifyDecisionSignature,
|
|
185
|
+
verifyToken
|
|
186
|
+
};
|
|
187
|
+
//# sourceMappingURL=chunk-OSVTQPQG.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 * `<kid>.<base64url>`: the API's Ed25519 signature over this decision, issued when the\n * ledger recorded it. Absent on a decision no ledger holds (a hand-written cookie).\n */\n sig?: 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/** Where the platform's public signing keys come from, for signatures and consent tokens. */\nexport interface KeySetOptions {\n /** A key set you already hold (`{ keys: [...] }`). Skips the fetch. */\n jwks?: JsonWebKeySet;\n /** Default `https://api.cookiemunch.net/v1/partner/jwks.json`. */\n jwksUrl?: string;\n /** Injectable fetch. Default: the global one. */\n fetch?: typeof fetch;\n}\n\n/** `readConsent` with `requireSigned: true`: only a decision the API signed counts. */\nexport interface ReadSignedConsentOptions extends ReadConsentOptions, KeySetOptions {\n requireSigned: true;\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 if (typeof r.sig === 'string') out.sig = r.sig;\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: ReadSignedConsentOptions): Promise<ServerConsent | null>;\nexport function readConsent(source: ConsentSource, opts?: ReadConsentOptions & { requireSigned?: false }): ServerConsent | null;\nexport function readConsent(\n source: ConsentSource,\n opts: ReadConsentOptions & { requireSigned?: boolean } & KeySetOptions = {},\n): ServerConsent | null | Promise<ServerConsent | null> {\n const consent = readUnsigned(source, opts);\n if (opts.requireSigned !== true) return consent;\n // CMU-S04: a decision counts only when the API's signature over it verifies against the\n // published key set. A hand-written cookie (no sig) or a forged one reads as no decision.\n if (!consent?.sig) return Promise.resolve(null);\n return verifyDecisionSignature(consent, opts).then((ok) => (ok ? consent : null));\n}\n\nfunction readUnsigned(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\n// ---- signatures (CMU-S04) and consent tokens (CMU-F06) ----\n\nexport const DEFAULT_JWKS_URL = 'https://api.cookiemunch.net/v1/partner/jwks.json';\n\n/** The fields of a decision its signature covers. */\nexport interface SignableDecision {\n stamp: string;\n ver: number;\n utc: number;\n preferences: boolean;\n statistics: boolean;\n marketing: boolean;\n custom?: Record<string, boolean> | undefined;\n}\n\n/**\n * The exact line the API signs for a recorded decision. Identical to @cookiemunch/core's\n * `consentSignaturePayload` (a server test pins the two together).\n */\nexport function consentSignaturePayload(d: SignableDecision): string {\n const b = (v: boolean | undefined): string => (v ? '1' : '0');\n const custom = d.custom ?? {};\n const named = Object.keys(custom)\n .sort()\n .map((k) => `${k}=${b(custom[k])}`)\n .join(',');\n return `cmc1|${d.stamp}|${d.ver}|${d.utc}|${b(d.preferences)}${b(d.statistics)}${b(d.marketing)}|${named}`;\n}\n\ninterface Jwk {\n kty?: string;\n crv?: string;\n x?: string;\n kid?: string;\n alg?: string;\n}\nexport interface JsonWebKeySet {\n keys: Jwk[];\n}\n\nconst jwksCache = new Map<string, { at: number; set: JsonWebKeySet }>();\nconst JWKS_TTL_MS = 60 * 60 * 1000;\n\nasync function keySet(opts: KeySetOptions, kid: string): Promise<JsonWebKeySet> {\n if (opts.jwks) return opts.jwks;\n const url = opts.jwksUrl ?? DEFAULT_JWKS_URL;\n const hit = jwksCache.get(url);\n // An unknown kid refetches: the key may have rotated since the cached copy.\n if (hit && Date.now() - hit.at < JWKS_TTL_MS && hit.set.keys.some((k) => k.kid === kid)) return hit.set;\n const f = opts.fetch ?? (globalThis as { fetch?: typeof fetch }).fetch;\n if (!f) throw new Error('no fetch available to load the key set; pass `jwks`');\n const res = await f(url);\n if (!res.ok) throw new Error(`key set ${url} answered ${res.status}`);\n const set = (await res.json()) as JsonWebKeySet;\n if (!set || !Array.isArray(set.keys)) throw new Error(`key set ${url} is not a JWKS`);\n jwksCache.set(url, { at: Date.now(), set });\n return set;\n}\n\nfunction b64urlBytes(s: string): Uint8Array {\n const b64 = s.replace(/-/g, '+').replace(/_/g, '/') + '==='.slice((s.length + 3) % 4);\n const bin = atob(b64);\n const out = new Uint8Array(bin.length);\n for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);\n return out;\n}\n\n/** Ed25519 verify with WebCrypto (Node 20+, Deno, Bun, workers, browsers). */\nasync function edVerify(jwk: Jwk, data: string, signature: string): Promise<boolean> {\n const subtle = (globalThis as { crypto?: Crypto }).crypto?.subtle;\n if (!subtle) throw new Error('WebCrypto is unavailable in this runtime');\n if (jwk.kty !== 'OKP' || jwk.crv !== 'Ed25519' || !jwk.x) return false;\n const key = await subtle.importKey('jwk', { kty: 'OKP', crv: 'Ed25519', x: jwk.x }, { name: 'Ed25519' }, false, ['verify']);\n return subtle.verify({ name: 'Ed25519' }, key, b64urlBytes(signature) as BufferSource, new TextEncoder().encode(data) as BufferSource);\n}\n\n/** Does the cookie's `sig` verify for this decision? False on anything malformed. */\nexport async function verifyDecisionSignature(consent: ServerConsent, opts: KeySetOptions = {}): Promise<boolean> {\n const dot = consent.sig?.lastIndexOf('.') ?? -1;\n if (!consent.sig || dot <= 0) return false;\n const kid = consent.sig.slice(0, dot);\n try {\n const jwk = (await keySet(opts, kid)).keys.find((k) => k.kid === kid);\n if (!jwk) return false;\n return await edVerify(jwk, consentSignaturePayload(consent), consent.sig.slice(dot + 1));\n } catch {\n return false;\n }\n}\n\n/** What a consent token (`CookieMunch.getConsentToken()`) asserts. */\nexport interface ConsentTokenClaims {\n iss: string;\n /** The decision's stamp. */\n sub: string;\n /** `cookiemunch:<cbid>`. */\n aud: string;\n token_use: 'consent';\n cbid: string;\n stamp: string;\n /** Every category the decision answered, `necessary` included. */\n choices: Record<string, boolean>;\n ver: number;\n /** When the decision was made, epoch ms. */\n decidedAt: number;\n cause?: string;\n iat: number;\n exp: number;\n}\n\nexport interface VerifyTokenOptions extends KeySetOptions {\n /** Require the token to be for this site. */\n cbid?: string;\n /** Clock, epoch ms. Default `Date.now()`. */\n now?: number;\n /** Allowed clock skew, seconds. Default 30. */\n leewaySeconds?: number;\n}\n\n/**\n * Verify a consent token from `CookieMunch.getConsentToken()` (CMU-F06) against the published\n * key set and return its claims. Throws on a bad signature, a wrong site, or an expired token.\n */\nexport async function verifyToken(token: string, opts: VerifyTokenOptions = {}): Promise<ConsentTokenClaims> {\n const parts = typeof token === 'string' ? token.split('.') : [];\n if (parts.length !== 3) throw new Error('not a JWT');\n const [h, p, s] = parts as [string, string, string];\n let header: { alg?: string; kid?: string };\n let claims: ConsentTokenClaims;\n try {\n header = JSON.parse(new TextDecoder().decode(b64urlBytes(h)));\n claims = JSON.parse(new TextDecoder().decode(b64urlBytes(p)));\n } catch {\n throw new Error('malformed token');\n }\n if (header.alg !== 'EdDSA' || !header.kid) throw new Error('unexpected token algorithm');\n const jwk = (await keySet(opts, header.kid)).keys.find((k) => k.kid === header.kid);\n if (!jwk || !(await edVerify(jwk, `${h}.${p}`, s))) throw new Error('bad signature');\n if (claims.token_use !== 'consent') throw new Error('not a consent token');\n const now = (opts.now ?? Date.now()) / 1000;\n const leeway = opts.leewaySeconds ?? 30;\n if (typeof claims.exp !== 'number' || now > claims.exp + leeway) throw new Error('expired');\n if (typeof claims.iat === 'number' && claims.iat > now + leeway) throw new Error('issued in the future');\n if (opts.cbid !== undefined && (claims.cbid !== opts.cbid || claims.aud !== `cookiemunch:${opts.cbid}`)) throw new Error('token is for another site');\n return claims;\n}\n"],"mappings":"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","names":[]}
|
package/dist/consent.cjs
CHANGED
|
@@ -21,10 +21,14 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var consent_exports = {};
|
|
22
22
|
__export(consent_exports, {
|
|
23
23
|
CONSENT_COOKIE_NAME: () => CONSENT_COOKIE_NAME,
|
|
24
|
+
DEFAULT_JWKS_URL: () => DEFAULT_JWKS_URL,
|
|
25
|
+
consentSignaturePayload: () => consentSignaturePayload,
|
|
24
26
|
getConsent: () => getConsent,
|
|
25
27
|
isAllowed: () => isAllowed,
|
|
26
28
|
parseConsentValue: () => parseConsentValue,
|
|
27
|
-
readConsent: () => readConsent
|
|
29
|
+
readConsent: () => readConsent,
|
|
30
|
+
verifyDecisionSignature: () => verifyDecisionSignature,
|
|
31
|
+
verifyToken: () => verifyToken
|
|
28
32
|
});
|
|
29
33
|
module.exports = __toCommonJS(consent_exports);
|
|
30
34
|
var CONSENT_COOKIE_NAME = "CookieMunch";
|
|
@@ -82,6 +86,7 @@ function normalize(o) {
|
|
|
82
86
|
for (const [k, v] of Object.entries(r.custom)) if (bool(v)) custom[k] = v;
|
|
83
87
|
if (Object.keys(custom).length > 0) out.custom = custom;
|
|
84
88
|
}
|
|
89
|
+
if (typeof r.sig === "string") out.sig = r.sig;
|
|
85
90
|
return out;
|
|
86
91
|
}
|
|
87
92
|
function parseConsentValue(raw) {
|
|
@@ -101,6 +106,12 @@ function parseConsentValue(raw) {
|
|
|
101
106
|
}
|
|
102
107
|
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
103
108
|
function readConsent(source, opts = {}) {
|
|
109
|
+
const consent = readUnsigned(source, opts);
|
|
110
|
+
if (opts.requireSigned !== true) return consent;
|
|
111
|
+
if (!consent?.sig) return Promise.resolve(null);
|
|
112
|
+
return verifyDecisionSignature(consent, opts).then((ok) => ok ? consent : null);
|
|
113
|
+
}
|
|
114
|
+
function readUnsigned(source, opts) {
|
|
104
115
|
const raw = cookieValue(cookieHeaderOf(source), opts.cookieName ?? CONSENT_COOKIE_NAME);
|
|
105
116
|
const consent = raw ? parseConsentValue(raw) : null;
|
|
106
117
|
if (!consent) return null;
|
|
@@ -121,12 +132,88 @@ function isAllowed(consent, category) {
|
|
|
121
132
|
if (category === "marketing") return consent.marketing === true;
|
|
122
133
|
return consent.custom?.[category] === true;
|
|
123
134
|
}
|
|
135
|
+
var DEFAULT_JWKS_URL = "https://api.cookiemunch.net/v1/partner/jwks.json";
|
|
136
|
+
function consentSignaturePayload(d) {
|
|
137
|
+
const b = (v) => v ? "1" : "0";
|
|
138
|
+
const custom = d.custom ?? {};
|
|
139
|
+
const named = Object.keys(custom).sort().map((k) => `${k}=${b(custom[k])}`).join(",");
|
|
140
|
+
return `cmc1|${d.stamp}|${d.ver}|${d.utc}|${b(d.preferences)}${b(d.statistics)}${b(d.marketing)}|${named}`;
|
|
141
|
+
}
|
|
142
|
+
var jwksCache = /* @__PURE__ */ new Map();
|
|
143
|
+
var JWKS_TTL_MS = 60 * 60 * 1e3;
|
|
144
|
+
async function keySet(opts, kid) {
|
|
145
|
+
if (opts.jwks) return opts.jwks;
|
|
146
|
+
const url = opts.jwksUrl ?? DEFAULT_JWKS_URL;
|
|
147
|
+
const hit = jwksCache.get(url);
|
|
148
|
+
if (hit && Date.now() - hit.at < JWKS_TTL_MS && hit.set.keys.some((k) => k.kid === kid)) return hit.set;
|
|
149
|
+
const f = opts.fetch ?? globalThis.fetch;
|
|
150
|
+
if (!f) throw new Error("no fetch available to load the key set; pass `jwks`");
|
|
151
|
+
const res = await f(url);
|
|
152
|
+
if (!res.ok) throw new Error(`key set ${url} answered ${res.status}`);
|
|
153
|
+
const set = await res.json();
|
|
154
|
+
if (!set || !Array.isArray(set.keys)) throw new Error(`key set ${url} is not a JWKS`);
|
|
155
|
+
jwksCache.set(url, { at: Date.now(), set });
|
|
156
|
+
return set;
|
|
157
|
+
}
|
|
158
|
+
function b64urlBytes(s) {
|
|
159
|
+
const b64 = s.replace(/-/g, "+").replace(/_/g, "/") + "===".slice((s.length + 3) % 4);
|
|
160
|
+
const bin = atob(b64);
|
|
161
|
+
const out = new Uint8Array(bin.length);
|
|
162
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
165
|
+
async function edVerify(jwk, data, signature) {
|
|
166
|
+
const subtle = globalThis.crypto?.subtle;
|
|
167
|
+
if (!subtle) throw new Error("WebCrypto is unavailable in this runtime");
|
|
168
|
+
if (jwk.kty !== "OKP" || jwk.crv !== "Ed25519" || !jwk.x) return false;
|
|
169
|
+
const key = await subtle.importKey("jwk", { kty: "OKP", crv: "Ed25519", x: jwk.x }, { name: "Ed25519" }, false, ["verify"]);
|
|
170
|
+
return subtle.verify({ name: "Ed25519" }, key, b64urlBytes(signature), new TextEncoder().encode(data));
|
|
171
|
+
}
|
|
172
|
+
async function verifyDecisionSignature(consent, opts = {}) {
|
|
173
|
+
const dot = consent.sig?.lastIndexOf(".") ?? -1;
|
|
174
|
+
if (!consent.sig || dot <= 0) return false;
|
|
175
|
+
const kid = consent.sig.slice(0, dot);
|
|
176
|
+
try {
|
|
177
|
+
const jwk = (await keySet(opts, kid)).keys.find((k) => k.kid === kid);
|
|
178
|
+
if (!jwk) return false;
|
|
179
|
+
return await edVerify(jwk, consentSignaturePayload(consent), consent.sig.slice(dot + 1));
|
|
180
|
+
} catch {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
async function verifyToken(token, opts = {}) {
|
|
185
|
+
const parts = typeof token === "string" ? token.split(".") : [];
|
|
186
|
+
if (parts.length !== 3) throw new Error("not a JWT");
|
|
187
|
+
const [h, p, s] = parts;
|
|
188
|
+
let header;
|
|
189
|
+
let claims;
|
|
190
|
+
try {
|
|
191
|
+
header = JSON.parse(new TextDecoder().decode(b64urlBytes(h)));
|
|
192
|
+
claims = JSON.parse(new TextDecoder().decode(b64urlBytes(p)));
|
|
193
|
+
} catch {
|
|
194
|
+
throw new Error("malformed token");
|
|
195
|
+
}
|
|
196
|
+
if (header.alg !== "EdDSA" || !header.kid) throw new Error("unexpected token algorithm");
|
|
197
|
+
const jwk = (await keySet(opts, header.kid)).keys.find((k) => k.kid === header.kid);
|
|
198
|
+
if (!jwk || !await edVerify(jwk, `${h}.${p}`, s)) throw new Error("bad signature");
|
|
199
|
+
if (claims.token_use !== "consent") throw new Error("not a consent token");
|
|
200
|
+
const now = (opts.now ?? Date.now()) / 1e3;
|
|
201
|
+
const leeway = opts.leewaySeconds ?? 30;
|
|
202
|
+
if (typeof claims.exp !== "number" || now > claims.exp + leeway) throw new Error("expired");
|
|
203
|
+
if (typeof claims.iat === "number" && claims.iat > now + leeway) throw new Error("issued in the future");
|
|
204
|
+
if (opts.cbid !== void 0 && (claims.cbid !== opts.cbid || claims.aud !== `cookiemunch:${opts.cbid}`)) throw new Error("token is for another site");
|
|
205
|
+
return claims;
|
|
206
|
+
}
|
|
124
207
|
// Annotate the CommonJS export names for ESM import in node:
|
|
125
208
|
0 && (module.exports = {
|
|
126
209
|
CONSENT_COOKIE_NAME,
|
|
210
|
+
DEFAULT_JWKS_URL,
|
|
211
|
+
consentSignaturePayload,
|
|
127
212
|
getConsent,
|
|
128
213
|
isAllowed,
|
|
129
214
|
parseConsentValue,
|
|
130
|
-
readConsent
|
|
215
|
+
readConsent,
|
|
216
|
+
verifyDecisionSignature,
|
|
217
|
+
verifyToken
|
|
131
218
|
});
|
|
132
219
|
//# sourceMappingURL=consent.cjs.map
|
package/dist/consent.cjs.map
CHANGED
|
@@ -1 +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":[]}
|
|
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 * `<kid>.<base64url>`: the API's Ed25519 signature over this decision, issued when the\n * ledger recorded it. Absent on a decision no ledger holds (a hand-written cookie).\n */\n sig?: 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/** Where the platform's public signing keys come from, for signatures and consent tokens. */\nexport interface KeySetOptions {\n /** A key set you already hold (`{ keys: [...] }`). Skips the fetch. */\n jwks?: JsonWebKeySet;\n /** Default `https://api.cookiemunch.net/v1/partner/jwks.json`. */\n jwksUrl?: string;\n /** Injectable fetch. Default: the global one. */\n fetch?: typeof fetch;\n}\n\n/** `readConsent` with `requireSigned: true`: only a decision the API signed counts. */\nexport interface ReadSignedConsentOptions extends ReadConsentOptions, KeySetOptions {\n requireSigned: true;\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 if (typeof r.sig === 'string') out.sig = r.sig;\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: ReadSignedConsentOptions): Promise<ServerConsent | null>;\nexport function readConsent(source: ConsentSource, opts?: ReadConsentOptions & { requireSigned?: false }): ServerConsent | null;\nexport function readConsent(\n source: ConsentSource,\n opts: ReadConsentOptions & { requireSigned?: boolean } & KeySetOptions = {},\n): ServerConsent | null | Promise<ServerConsent | null> {\n const consent = readUnsigned(source, opts);\n if (opts.requireSigned !== true) return consent;\n // CMU-S04: a decision counts only when the API's signature over it verifies against the\n // published key set. A hand-written cookie (no sig) or a forged one reads as no decision.\n if (!consent?.sig) return Promise.resolve(null);\n return verifyDecisionSignature(consent, opts).then((ok) => (ok ? consent : null));\n}\n\nfunction readUnsigned(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\n// ---- signatures (CMU-S04) and consent tokens (CMU-F06) ----\n\nexport const DEFAULT_JWKS_URL = 'https://api.cookiemunch.net/v1/partner/jwks.json';\n\n/** The fields of a decision its signature covers. */\nexport interface SignableDecision {\n stamp: string;\n ver: number;\n utc: number;\n preferences: boolean;\n statistics: boolean;\n marketing: boolean;\n custom?: Record<string, boolean> | undefined;\n}\n\n/**\n * The exact line the API signs for a recorded decision. Identical to @cookiemunch/core's\n * `consentSignaturePayload` (a server test pins the two together).\n */\nexport function consentSignaturePayload(d: SignableDecision): string {\n const b = (v: boolean | undefined): string => (v ? '1' : '0');\n const custom = d.custom ?? {};\n const named = Object.keys(custom)\n .sort()\n .map((k) => `${k}=${b(custom[k])}`)\n .join(',');\n return `cmc1|${d.stamp}|${d.ver}|${d.utc}|${b(d.preferences)}${b(d.statistics)}${b(d.marketing)}|${named}`;\n}\n\ninterface Jwk {\n kty?: string;\n crv?: string;\n x?: string;\n kid?: string;\n alg?: string;\n}\nexport interface JsonWebKeySet {\n keys: Jwk[];\n}\n\nconst jwksCache = new Map<string, { at: number; set: JsonWebKeySet }>();\nconst JWKS_TTL_MS = 60 * 60 * 1000;\n\nasync function keySet(opts: KeySetOptions, kid: string): Promise<JsonWebKeySet> {\n if (opts.jwks) return opts.jwks;\n const url = opts.jwksUrl ?? DEFAULT_JWKS_URL;\n const hit = jwksCache.get(url);\n // An unknown kid refetches: the key may have rotated since the cached copy.\n if (hit && Date.now() - hit.at < JWKS_TTL_MS && hit.set.keys.some((k) => k.kid === kid)) return hit.set;\n const f = opts.fetch ?? (globalThis as { fetch?: typeof fetch }).fetch;\n if (!f) throw new Error('no fetch available to load the key set; pass `jwks`');\n const res = await f(url);\n if (!res.ok) throw new Error(`key set ${url} answered ${res.status}`);\n const set = (await res.json()) as JsonWebKeySet;\n if (!set || !Array.isArray(set.keys)) throw new Error(`key set ${url} is not a JWKS`);\n jwksCache.set(url, { at: Date.now(), set });\n return set;\n}\n\nfunction b64urlBytes(s: string): Uint8Array {\n const b64 = s.replace(/-/g, '+').replace(/_/g, '/') + '==='.slice((s.length + 3) % 4);\n const bin = atob(b64);\n const out = new Uint8Array(bin.length);\n for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);\n return out;\n}\n\n/** Ed25519 verify with WebCrypto (Node 20+, Deno, Bun, workers, browsers). */\nasync function edVerify(jwk: Jwk, data: string, signature: string): Promise<boolean> {\n const subtle = (globalThis as { crypto?: Crypto }).crypto?.subtle;\n if (!subtle) throw new Error('WebCrypto is unavailable in this runtime');\n if (jwk.kty !== 'OKP' || jwk.crv !== 'Ed25519' || !jwk.x) return false;\n const key = await subtle.importKey('jwk', { kty: 'OKP', crv: 'Ed25519', x: jwk.x }, { name: 'Ed25519' }, false, ['verify']);\n return subtle.verify({ name: 'Ed25519' }, key, b64urlBytes(signature) as BufferSource, new TextEncoder().encode(data) as BufferSource);\n}\n\n/** Does the cookie's `sig` verify for this decision? False on anything malformed. */\nexport async function verifyDecisionSignature(consent: ServerConsent, opts: KeySetOptions = {}): Promise<boolean> {\n const dot = consent.sig?.lastIndexOf('.') ?? -1;\n if (!consent.sig || dot <= 0) return false;\n const kid = consent.sig.slice(0, dot);\n try {\n const jwk = (await keySet(opts, kid)).keys.find((k) => k.kid === kid);\n if (!jwk) return false;\n return await edVerify(jwk, consentSignaturePayload(consent), consent.sig.slice(dot + 1));\n } catch {\n return false;\n }\n}\n\n/** What a consent token (`CookieMunch.getConsentToken()`) asserts. */\nexport interface ConsentTokenClaims {\n iss: string;\n /** The decision's stamp. */\n sub: string;\n /** `cookiemunch:<cbid>`. */\n aud: string;\n token_use: 'consent';\n cbid: string;\n stamp: string;\n /** Every category the decision answered, `necessary` included. */\n choices: Record<string, boolean>;\n ver: number;\n /** When the decision was made, epoch ms. */\n decidedAt: number;\n cause?: string;\n iat: number;\n exp: number;\n}\n\nexport interface VerifyTokenOptions extends KeySetOptions {\n /** Require the token to be for this site. */\n cbid?: string;\n /** Clock, epoch ms. Default `Date.now()`. */\n now?: number;\n /** Allowed clock skew, seconds. Default 30. */\n leewaySeconds?: number;\n}\n\n/**\n * Verify a consent token from `CookieMunch.getConsentToken()` (CMU-F06) against the published\n * key set and return its claims. Throws on a bad signature, a wrong site, or an expired token.\n */\nexport async function verifyToken(token: string, opts: VerifyTokenOptions = {}): Promise<ConsentTokenClaims> {\n const parts = typeof token === 'string' ? token.split('.') : [];\n if (parts.length !== 3) throw new Error('not a JWT');\n const [h, p, s] = parts as [string, string, string];\n let header: { alg?: string; kid?: string };\n let claims: ConsentTokenClaims;\n try {\n header = JSON.parse(new TextDecoder().decode(b64urlBytes(h)));\n claims = JSON.parse(new TextDecoder().decode(b64urlBytes(p)));\n } catch {\n throw new Error('malformed token');\n }\n if (header.alg !== 'EdDSA' || !header.kid) throw new Error('unexpected token algorithm');\n const jwk = (await keySet(opts, header.kid)).keys.find((k) => k.kid === header.kid);\n if (!jwk || !(await edVerify(jwk, `${h}.${p}`, s))) throw new Error('bad signature');\n if (claims.token_use !== 'consent') throw new Error('not a consent token');\n const now = (opts.now ?? Date.now()) / 1000;\n const leeway = opts.leewaySeconds ?? 30;\n if (typeof claims.exp !== 'number' || now > claims.exp + leeway) throw new Error('expired');\n if (typeof claims.iat === 'number' && claims.iat > now + leeway) throw new Error('issued in the future');\n if (opts.cbid !== undefined && (claims.cbid !== opts.cbid || claims.aud !== `cookiemunch:${opts.cbid}`)) throw new Error('token is for another site');\n return claims;\n}\n"],"mappings":"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","names":[]}
|
package/dist/consent.d.cts
CHANGED
|
@@ -36,6 +36,11 @@ interface ServerConsent {
|
|
|
36
36
|
region: string;
|
|
37
37
|
/** The anonymous consent-receipt id tying this browser to the consent log. */
|
|
38
38
|
stamp: string;
|
|
39
|
+
/**
|
|
40
|
+
* `<kid>.<base64url>`: the API's Ed25519 signature over this decision, issued when the
|
|
41
|
+
* ledger recorded it. Absent on a decision no ledger holds (a hand-written cookie).
|
|
42
|
+
*/
|
|
43
|
+
sig?: string;
|
|
39
44
|
}
|
|
40
45
|
/** The site's consent policy, so the server applies the same rules the embed does. */
|
|
41
46
|
interface ReadConsentOptions {
|
|
@@ -52,6 +57,19 @@ interface ReadConsentOptions {
|
|
|
52
57
|
/** Clock for `expiryDays`, epoch ms. Default `Date.now()`. */
|
|
53
58
|
now?: number;
|
|
54
59
|
}
|
|
60
|
+
/** Where the platform's public signing keys come from, for signatures and consent tokens. */
|
|
61
|
+
interface KeySetOptions {
|
|
62
|
+
/** A key set you already hold (`{ keys: [...] }`). Skips the fetch. */
|
|
63
|
+
jwks?: JsonWebKeySet;
|
|
64
|
+
/** Default `https://api.cookiemunch.net/v1/partner/jwks.json`. */
|
|
65
|
+
jwksUrl?: string;
|
|
66
|
+
/** Injectable fetch. Default: the global one. */
|
|
67
|
+
fetch?: typeof fetch;
|
|
68
|
+
}
|
|
69
|
+
/** `readConsent` with `requireSigned: true`: only a decision the API signed counts. */
|
|
70
|
+
interface ReadSignedConsentOptions extends ReadConsentOptions, KeySetOptions {
|
|
71
|
+
requireSigned: true;
|
|
72
|
+
}
|
|
55
73
|
/** Anything we can pull a Cookie header off of. */
|
|
56
74
|
type ConsentSource = string | null | undefined | {
|
|
57
75
|
headers?: {
|
|
@@ -70,7 +88,10 @@ declare function parseConsentValue(raw: string): ServerConsent | null;
|
|
|
70
88
|
* `Request`. Returns null when there is no decision the embed would honour — no cookie, an
|
|
71
89
|
* unparseable one, or (with the options) one made under an older version or past expiry.
|
|
72
90
|
*/
|
|
73
|
-
declare function readConsent(source: ConsentSource, opts
|
|
91
|
+
declare function readConsent(source: ConsentSource, opts: ReadSignedConsentOptions): Promise<ServerConsent | null>;
|
|
92
|
+
declare function readConsent(source: ConsentSource, opts?: ReadConsentOptions & {
|
|
93
|
+
requireSigned?: false;
|
|
94
|
+
}): ServerConsent | null;
|
|
74
95
|
/**
|
|
75
96
|
* Read the visitor's consent from a request. The original spelling of {@link readConsent},
|
|
76
97
|
* kept for existing callers; the second argument is the cookie name.
|
|
@@ -84,5 +105,65 @@ type ConsentCategory = 'necessary' | 'preferences' | 'statistics' | 'marketing'
|
|
|
84
105
|
* categories (fail-closed / prior-blocking posture), matching the embed.
|
|
85
106
|
*/
|
|
86
107
|
declare function isAllowed(consent: ServerConsent | null, category: ConsentCategory): boolean;
|
|
108
|
+
declare const DEFAULT_JWKS_URL = "https://api.cookiemunch.net/v1/partner/jwks.json";
|
|
109
|
+
/** The fields of a decision its signature covers. */
|
|
110
|
+
interface SignableDecision {
|
|
111
|
+
stamp: string;
|
|
112
|
+
ver: number;
|
|
113
|
+
utc: number;
|
|
114
|
+
preferences: boolean;
|
|
115
|
+
statistics: boolean;
|
|
116
|
+
marketing: boolean;
|
|
117
|
+
custom?: Record<string, boolean> | undefined;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* The exact line the API signs for a recorded decision. Identical to @cookiemunch/core's
|
|
121
|
+
* `consentSignaturePayload` (a server test pins the two together).
|
|
122
|
+
*/
|
|
123
|
+
declare function consentSignaturePayload(d: SignableDecision): string;
|
|
124
|
+
interface Jwk {
|
|
125
|
+
kty?: string;
|
|
126
|
+
crv?: string;
|
|
127
|
+
x?: string;
|
|
128
|
+
kid?: string;
|
|
129
|
+
alg?: string;
|
|
130
|
+
}
|
|
131
|
+
interface JsonWebKeySet {
|
|
132
|
+
keys: Jwk[];
|
|
133
|
+
}
|
|
134
|
+
/** Does the cookie's `sig` verify for this decision? False on anything malformed. */
|
|
135
|
+
declare function verifyDecisionSignature(consent: ServerConsent, opts?: KeySetOptions): Promise<boolean>;
|
|
136
|
+
/** What a consent token (`CookieMunch.getConsentToken()`) asserts. */
|
|
137
|
+
interface ConsentTokenClaims {
|
|
138
|
+
iss: string;
|
|
139
|
+
/** The decision's stamp. */
|
|
140
|
+
sub: string;
|
|
141
|
+
/** `cookiemunch:<cbid>`. */
|
|
142
|
+
aud: string;
|
|
143
|
+
token_use: 'consent';
|
|
144
|
+
cbid: string;
|
|
145
|
+
stamp: string;
|
|
146
|
+
/** Every category the decision answered, `necessary` included. */
|
|
147
|
+
choices: Record<string, boolean>;
|
|
148
|
+
ver: number;
|
|
149
|
+
/** When the decision was made, epoch ms. */
|
|
150
|
+
decidedAt: number;
|
|
151
|
+
cause?: string;
|
|
152
|
+
iat: number;
|
|
153
|
+
exp: number;
|
|
154
|
+
}
|
|
155
|
+
interface VerifyTokenOptions extends KeySetOptions {
|
|
156
|
+
/** Require the token to be for this site. */
|
|
157
|
+
cbid?: string;
|
|
158
|
+
/** Clock, epoch ms. Default `Date.now()`. */
|
|
159
|
+
now?: number;
|
|
160
|
+
/** Allowed clock skew, seconds. Default 30. */
|
|
161
|
+
leewaySeconds?: number;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Verify a consent token from `CookieMunch.getConsentToken()` (CMU-F06) against the published
|
|
165
|
+
* key set and return its claims. Throws on a bad signature, a wrong site, or an expired token.
|
|
166
|
+
*/
|
|
167
|
+
declare function verifyToken(token: string, opts?: VerifyTokenOptions): Promise<ConsentTokenClaims>;
|
|
87
168
|
|
|
88
|
-
export { CONSENT_COOKIE_NAME, type ConsentCategory, type ConsentSource, type ReadConsentOptions, type ServerConsent, getConsent, isAllowed, parseConsentValue, readConsent };
|
|
169
|
+
export { CONSENT_COOKIE_NAME, type ConsentCategory, type ConsentSource, type ConsentTokenClaims, DEFAULT_JWKS_URL, type JsonWebKeySet, type KeySetOptions, type ReadConsentOptions, type ReadSignedConsentOptions, type ServerConsent, type SignableDecision, type VerifyTokenOptions, consentSignaturePayload, getConsent, isAllowed, parseConsentValue, readConsent, verifyDecisionSignature, verifyToken };
|