@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/dist/index.cjs CHANGED
@@ -20,15 +20,25 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var src_exports = {};
22
22
  __export(src_exports, {
23
+ CANARY_ISSUER: () => CANARY_ISSUER,
24
+ CONFORMANCE_RULES: () => CONFORMANCE_RULES,
23
25
  CONSENT_COOKIE_NAME: () => CONSENT_COOKIE_NAME,
24
26
  CookieMunchApiError: () => CookieMunchApiError,
27
+ DEFAULT_JWKS_URL: () => DEFAULT_JWKS_URL,
25
28
  SDK_VERSION: () => SDK_VERSION,
29
+ TokenExchangeError: () => TokenExchangeError,
30
+ canaryIdentityFor: () => canaryIdentityFor,
31
+ consentSignaturePayload: () => consentSignaturePayload,
26
32
  createCookieMunch: () => createCookieMunch,
33
+ exchangeOperatorToken: () => exchangeOperatorToken,
27
34
  getConsent: () => getConsent,
28
35
  isAllowed: () => isAllowed,
29
36
  mintConsentSyncToken: () => mintConsentSyncToken,
30
37
  parseConsentValue: () => parseConsentValue,
31
38
  readConsent: () => readConsent,
39
+ runExecutorConformance: () => runExecutorConformance,
40
+ verifyDecisionSignature: () => verifyDecisionSignature,
41
+ verifyToken: () => verifyToken,
32
42
  verifyWebhookSignature: () => verifyWebhookSignature
33
43
  });
34
44
  module.exports = __toCommonJS(src_exports);
@@ -89,6 +99,7 @@ function normalize(o) {
89
99
  for (const [k, v] of Object.entries(r.custom)) if (bool(v)) custom[k] = v;
90
100
  if (Object.keys(custom).length > 0) out.custom = custom;
91
101
  }
102
+ if (typeof r.sig === "string") out.sig = r.sig;
92
103
  return out;
93
104
  }
94
105
  function parseConsentValue(raw) {
@@ -108,6 +119,12 @@ function parseConsentValue(raw) {
108
119
  }
109
120
  var DAY_MS = 24 * 60 * 60 * 1e3;
110
121
  function readConsent(source, opts = {}) {
122
+ const consent = readUnsigned(source, opts);
123
+ if (opts.requireSigned !== true) return consent;
124
+ if (!consent?.sig) return Promise.resolve(null);
125
+ return verifyDecisionSignature(consent, opts).then((ok) => ok ? consent : null);
126
+ }
127
+ function readUnsigned(source, opts) {
111
128
  const raw = cookieValue(cookieHeaderOf(source), opts.cookieName ?? CONSENT_COOKIE_NAME);
112
129
  const consent = raw ? parseConsentValue(raw) : null;
113
130
  if (!consent) return null;
@@ -128,6 +145,78 @@ function isAllowed(consent, category) {
128
145
  if (category === "marketing") return consent.marketing === true;
129
146
  return consent.custom?.[category] === true;
130
147
  }
148
+ var DEFAULT_JWKS_URL = "https://api.cookiemunch.net/v1/partner/jwks.json";
149
+ function consentSignaturePayload(d) {
150
+ const b = (v) => v ? "1" : "0";
151
+ const custom = d.custom ?? {};
152
+ const named = Object.keys(custom).sort().map((k) => `${k}=${b(custom[k])}`).join(",");
153
+ return `cmc1|${d.stamp}|${d.ver}|${d.utc}|${b(d.preferences)}${b(d.statistics)}${b(d.marketing)}|${named}`;
154
+ }
155
+ var jwksCache = /* @__PURE__ */ new Map();
156
+ var JWKS_TTL_MS = 60 * 60 * 1e3;
157
+ async function keySet(opts, kid) {
158
+ if (opts.jwks) return opts.jwks;
159
+ const url = opts.jwksUrl ?? DEFAULT_JWKS_URL;
160
+ const hit = jwksCache.get(url);
161
+ if (hit && Date.now() - hit.at < JWKS_TTL_MS && hit.set.keys.some((k) => k.kid === kid)) return hit.set;
162
+ const f = opts.fetch ?? globalThis.fetch;
163
+ if (!f) throw new Error("no fetch available to load the key set; pass `jwks`");
164
+ const res = await f(url);
165
+ if (!res.ok) throw new Error(`key set ${url} answered ${res.status}`);
166
+ const set = await res.json();
167
+ if (!set || !Array.isArray(set.keys)) throw new Error(`key set ${url} is not a JWKS`);
168
+ jwksCache.set(url, { at: Date.now(), set });
169
+ return set;
170
+ }
171
+ function b64urlBytes(s) {
172
+ const b64 = s.replace(/-/g, "+").replace(/_/g, "/") + "===".slice((s.length + 3) % 4);
173
+ const bin = atob(b64);
174
+ const out = new Uint8Array(bin.length);
175
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
176
+ return out;
177
+ }
178
+ async function edVerify(jwk, data, signature) {
179
+ const subtle = globalThis.crypto?.subtle;
180
+ if (!subtle) throw new Error("WebCrypto is unavailable in this runtime");
181
+ if (jwk.kty !== "OKP" || jwk.crv !== "Ed25519" || !jwk.x) return false;
182
+ const key = await subtle.importKey("jwk", { kty: "OKP", crv: "Ed25519", x: jwk.x }, { name: "Ed25519" }, false, ["verify"]);
183
+ return subtle.verify({ name: "Ed25519" }, key, b64urlBytes(signature), new TextEncoder().encode(data));
184
+ }
185
+ async function verifyDecisionSignature(consent, opts = {}) {
186
+ const dot = consent.sig?.lastIndexOf(".") ?? -1;
187
+ if (!consent.sig || dot <= 0) return false;
188
+ const kid = consent.sig.slice(0, dot);
189
+ try {
190
+ const jwk = (await keySet(opts, kid)).keys.find((k) => k.kid === kid);
191
+ if (!jwk) return false;
192
+ return await edVerify(jwk, consentSignaturePayload(consent), consent.sig.slice(dot + 1));
193
+ } catch {
194
+ return false;
195
+ }
196
+ }
197
+ async function verifyToken(token, opts = {}) {
198
+ const parts = typeof token === "string" ? token.split(".") : [];
199
+ if (parts.length !== 3) throw new Error("not a JWT");
200
+ const [h, p, s] = parts;
201
+ let header;
202
+ let claims;
203
+ try {
204
+ header = JSON.parse(new TextDecoder().decode(b64urlBytes(h)));
205
+ claims = JSON.parse(new TextDecoder().decode(b64urlBytes(p)));
206
+ } catch {
207
+ throw new Error("malformed token");
208
+ }
209
+ if (header.alg !== "EdDSA" || !header.kid) throw new Error("unexpected token algorithm");
210
+ const jwk = (await keySet(opts, header.kid)).keys.find((k) => k.kid === header.kid);
211
+ if (!jwk || !await edVerify(jwk, `${h}.${p}`, s)) throw new Error("bad signature");
212
+ if (claims.token_use !== "consent") throw new Error("not a consent token");
213
+ const now = (opts.now ?? Date.now()) / 1e3;
214
+ const leeway = opts.leewaySeconds ?? 30;
215
+ if (typeof claims.exp !== "number" || now > claims.exp + leeway) throw new Error("expired");
216
+ if (typeof claims.iat === "number" && claims.iat > now + leeway) throw new Error("issued in the future");
217
+ if (opts.cbid !== void 0 && (claims.cbid !== opts.cbid || claims.aud !== `cookiemunch:${opts.cbid}`)) throw new Error("token is for another site");
218
+ return claims;
219
+ }
131
220
 
132
221
  // src/webhooks.ts
133
222
  var import_node_crypto = require("crypto");
@@ -164,8 +253,219 @@ function mintConsentSyncToken(input) {
164
253
  return `cms1.${payload}.${sig}`;
165
254
  }
166
255
 
256
+ // src/executor-conformance.ts
257
+ var CANARY_ISSUER = "https://canary.cookiemunch.net";
258
+ var CANARY_EMAIL_RE = /^canary-([0-9a-f]{16})@canary\.cookiemunch\.net$/;
259
+ function canaryIdentityFor(canaryEmail) {
260
+ const m = CANARY_EMAIL_RE.exec(canaryEmail.trim().toLowerCase());
261
+ if (!m) throw new Error("canaryEmail must be the canary address canary-<16 hex>@canary.cookiemunch.net");
262
+ return { iss: CANARY_ISSUER, sub: `canary_${m[1]}`, ext: `canary:${m[1]}` };
263
+ }
264
+ var CONFORMANCE_RULES = [
265
+ { id: "auth.required", title: "A request without the credential is refused (401 or 403)." },
266
+ { id: "lookup.email.found", title: "The canary subject is found by its email." },
267
+ { id: "lookup.identity.found", title: "The canary subject is found by its identity alone (iss + sub), and it is the same subject." },
268
+ { id: "lookup.unknown.no_match", title: 'An address nobody has answers { subjectId: null, status: "no_match" }.' },
269
+ { id: "lookup.identity_beats_email", title: "An identity that matches no one is never matched through the email of someone else." },
270
+ { id: "requests.bad_body.400", title: "A request with an unknown type answers 400 { error, field }." },
271
+ { id: "requests.alias.canonical_type", title: 'An alias (erasure) is accepted and answered with canonicalType "deletion".' },
272
+ { id: "requests.execute_after.scheduled", title: 'A deletion with a future executeAfter is "scheduled" with eta equal to executeAfter \u2014 nothing is deleted before it.' },
273
+ { id: "requests.idempotency.replay", title: "The same Idempotency-Key with the same body returns the same request, not a second one." },
274
+ { id: "requests.idempotency.mismatch_409", title: "The same Idempotency-Key with a different body answers 409." },
275
+ { id: "requests.get", title: "GET /privacy/requests/{id} returns the request, still scheduled." },
276
+ { id: "requests.unknown.404", title: "An unknown request id answers 404." },
277
+ { id: "requests.cancel.before_execute_after", title: 'A cancel before executeAfter answers status "cancelled".' },
278
+ { id: "requests.cancel.keeps_data", title: "After a cancel the subject is still there: a lookup finds it." },
279
+ { id: "version.header", title: "Answers carry X-Version (the highest contract version the executor supports)." }
280
+ ];
281
+ function randomTag() {
282
+ const bytes = new Uint8Array(8);
283
+ const c = globalThis.crypto;
284
+ if (c?.getRandomValues) c.getRandomValues(bytes);
285
+ else for (let i = 0; i < bytes.length; i++) bytes[i] = Math.floor(Math.random() * 256);
286
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
287
+ }
288
+ function field(json, key) {
289
+ return json && typeof json === "object" ? json[key] : void 0;
290
+ }
291
+ async function runExecutorConformance(o) {
292
+ const doFetch = o.fetch ?? fetch;
293
+ const now = o.now ?? (() => Date.now());
294
+ const base = o.baseUrl.replace(/\/+$/, "") + (o.prefix ?? "/privacy");
295
+ let url;
296
+ try {
297
+ url = new URL(base);
298
+ } catch {
299
+ throw new Error("baseUrl must be an absolute URL");
300
+ }
301
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && /^(localhost|127\.0\.0\.1|\[::1\])$/.test(url.hostname))) {
302
+ throw new Error("baseUrl must be https (http only for localhost)");
303
+ }
304
+ const identity = canaryIdentityFor(o.canaryEmail);
305
+ const canaryEmail = o.canaryEmail.trim().toLowerCase();
306
+ const auth = o.auth ?? { header: "Authorization", format: "Bearer {secret}" };
307
+ const run = o.runId ?? randomTag();
308
+ const results = /* @__PURE__ */ new Map();
309
+ let version;
310
+ const set = (id, status, detail) => {
311
+ const rule = CONFORMANCE_RULES.find((r) => r.id === id);
312
+ results.set(id, { id, title: rule.title, status, ...detail ? { detail } : {} });
313
+ };
314
+ const pass = (id) => set(id, "pass");
315
+ const fail = (id, detail) => set(id, "fail", detail);
316
+ const skip = (id, detail) => set(id, "skip", detail);
317
+ async function call(method, path, body, extra = {}, withAuth = true) {
318
+ const headers = {
319
+ accept: "application/json",
320
+ "X-Version": "2",
321
+ ...withAuth ? { [auth.header]: auth.format.replace("{secret}", o.secret) } : {},
322
+ ...body !== void 0 ? { "content-type": "application/json" } : {},
323
+ ...extra
324
+ };
325
+ try {
326
+ const res = await doFetch(base + path, { method, headers, ...body !== void 0 ? { body: JSON.stringify(body) } : {} });
327
+ const text = await res.text().catch(() => "");
328
+ let json;
329
+ try {
330
+ json = text ? JSON.parse(text) : void 0;
331
+ } catch {
332
+ json = void 0;
333
+ }
334
+ const v = res.headers.get("x-version");
335
+ if (v && !version) version = v;
336
+ return { status: res.status, json, version: v };
337
+ } catch (err) {
338
+ return { status: 0, json: void 0, version: null, error: err instanceof Error ? err.message : String(err) };
339
+ }
340
+ }
341
+ const said = (a) => a.status === 0 ? `unreachable: ${a.error}` : `HTTP ${a.status} ${JSON.stringify(a.json ?? null).slice(0, 200)}`;
342
+ const key = (rule) => `conformance:${run}:${rule}`;
343
+ const anon = await call("POST", "/lookup", { emails: [canaryEmail] }, {}, false);
344
+ if (anon.status === 0) {
345
+ for (const r of CONFORMANCE_RULES) skip(r.id, said(anon));
346
+ fail("auth.required", said(anon));
347
+ return report();
348
+ }
349
+ if (anon.status === 401 || anon.status === 403) pass("auth.required");
350
+ else fail("auth.required", `without a credential: ${said(anon)}`);
351
+ const byEmail = await call("POST", "/lookup", { emails: [canaryEmail] }, { "Idempotency-Key": key("lookup-email") });
352
+ const subjectId = field(byEmail.json, "subjectId");
353
+ if (byEmail.status === 200 && field(byEmail.json, "status") === "found" && typeof subjectId === "string" && subjectId) pass("lookup.email.found");
354
+ else fail("lookup.email.found", `the canary address: ${said(byEmail)}`);
355
+ const byIdentity = await call("POST", "/lookup", { emails: [], identity, atlas: identity }, { "Idempotency-Key": key("lookup-identity") });
356
+ if (byIdentity.status === 200 && field(byIdentity.json, "status") === "found" && field(byIdentity.json, "subjectId") === subjectId && typeof subjectId === "string") {
357
+ pass("lookup.identity.found");
358
+ } else {
359
+ fail("lookup.identity.found", `the canary identity ${identity.sub}: ${said(byIdentity)}${typeof subjectId === "string" ? ` (by email it was ${subjectId})` : ""}`);
360
+ }
361
+ const nobody = await call("POST", "/lookup", { emails: [`nobody-${run}@canary.cookiemunch.net`] }, { "Idempotency-Key": key("lookup-nobody") });
362
+ if (nobody.status === 200 && field(nobody.json, "status") === "no_match" && field(nobody.json, "subjectId") === null) pass("lookup.unknown.no_match");
363
+ else fail("lookup.unknown.no_match", said(nobody));
364
+ const stranger = { iss: identity.iss, sub: `canary_other_${run}` };
365
+ const crossed = await call("POST", "/lookup", { emails: [canaryEmail], identity: stranger, atlas: stranger }, { "Idempotency-Key": key("lookup-crossed") });
366
+ if (crossed.status === 200 && field(crossed.json, "status") === "no_match") pass("lookup.identity_beats_email");
367
+ else {
368
+ fail(
369
+ "lookup.identity_beats_email",
370
+ `an identity nobody has (${stranger.sub}) with the canary's email: ${said(crossed)} \u2014 the email belongs to a different identity, so this must be no_match`
371
+ );
372
+ }
373
+ const bad = await call("POST", "/requests", { subjectId: subjectId ?? "x", type: "teleport", caseId: `conformance-${run}` }, { "Idempotency-Key": key("bad-body") });
374
+ if (bad.status === 400 && typeof field(bad.json, "error") === "string" && typeof field(bad.json, "field") === "string") pass("requests.bad_body.400");
375
+ else fail("requests.bad_body.400", said(bad));
376
+ if (typeof subjectId !== "string") {
377
+ for (const id of ["requests.alias.canonical_type", "requests.execute_after.scheduled", "requests.idempotency.replay", "requests.idempotency.mismatch_409", "requests.get", "requests.cancel.before_execute_after", "requests.cancel.keeps_data"]) {
378
+ skip(id, "needs the canary subject, which the lookup did not find");
379
+ }
380
+ } else {
381
+ const executeAfter = new Date(Math.ceil((now() + 36e5) / 1e3) * 1e3).toISOString();
382
+ const body = { subjectId, type: "erasure", executeAfter, caseId: `conformance-${run}`, reason: "Cookie Munch conformance check (canary subject)" };
383
+ const opened = await call("POST", "/requests", body, { "Idempotency-Key": key("open") });
384
+ const id = field(opened.json, "id");
385
+ const st = field(opened.json, "status");
386
+ if ((opened.status === 200 || opened.status === 201) && field(opened.json, "canonicalType") === "deletion") pass("requests.alias.canonical_type");
387
+ else fail("requests.alias.canonical_type", `type "erasure": ${said(opened)}`);
388
+ const eta = field(opened.json, "eta");
389
+ if ((opened.status === 200 || opened.status === 201) && st === "scheduled" && typeof eta === "string" && Date.parse(eta) === Date.parse(executeAfter)) {
390
+ pass("requests.execute_after.scheduled");
391
+ } else if (st === "completed" || st === "completed_with_retention" || st === "processing") {
392
+ fail("requests.execute_after.scheduled", `executeAfter is ${executeAfter}, an hour away, and the executor answered "${String(st)}": it acted before executeAfter`);
393
+ } else {
394
+ fail("requests.execute_after.scheduled", `expected status "scheduled" with eta ${executeAfter}: ${said(opened)}`);
395
+ }
396
+ const replay = await call("POST", "/requests", body, { "Idempotency-Key": key("open") });
397
+ if (typeof id === "string" && (replay.status === 200 || replay.status === 201) && field(replay.json, "id") === id) pass("requests.idempotency.replay");
398
+ else fail("requests.idempotency.replay", `a retry with the same key: ${said(replay)}${typeof id === "string" ? ` (first answer id ${id})` : ""}`);
399
+ const mismatch = await call("POST", "/requests", { ...body, reason: "a different body" }, { "Idempotency-Key": key("open") });
400
+ if (mismatch.status === 409) pass("requests.idempotency.mismatch_409");
401
+ else fail("requests.idempotency.mismatch_409", `the same key with a different body: ${said(mismatch)}`);
402
+ if (typeof id !== "string") {
403
+ skip("requests.get", "no request id came back");
404
+ skip("requests.cancel.before_execute_after", "no request id came back");
405
+ skip("requests.cancel.keeps_data", "no request id came back");
406
+ } else {
407
+ const got = await call("GET", `/requests/${encodeURIComponent(id)}`, void 0, { "Idempotency-Key": key("get") });
408
+ if (got.status === 200 && field(got.json, "id") === id && field(got.json, "status") === "scheduled") pass("requests.get");
409
+ else fail("requests.get", said(got));
410
+ const cancelled = await call("POST", `/requests/${encodeURIComponent(id)}/cancel`, {}, { "Idempotency-Key": key("cancel") });
411
+ if (cancelled.status === 200 && field(cancelled.json, "status") === "cancelled") pass("requests.cancel.before_execute_after");
412
+ else fail("requests.cancel.before_execute_after", said(cancelled));
413
+ const after = await call("POST", "/lookup", { emails: [canaryEmail] }, { "Idempotency-Key": key("lookup-after-cancel") });
414
+ if (after.status === 200 && field(after.json, "status") === "found") pass("requests.cancel.keeps_data");
415
+ else fail("requests.cancel.keeps_data", `a lookup after the cancel: ${said(after)} \u2014 a cancel before executeAfter must leave the data intact`);
416
+ }
417
+ }
418
+ const unknown = await call("GET", `/requests/conformance-missing-${run}`, void 0, { "Idempotency-Key": key("unknown") });
419
+ if (unknown.status === 404) pass("requests.unknown.404");
420
+ else fail("requests.unknown.404", said(unknown));
421
+ if (version) pass("version.header");
422
+ else fail("version.header", "no answer carried an X-Version header");
423
+ return report();
424
+ function report() {
425
+ const rules = CONFORMANCE_RULES.map((r) => results.get(r.id) ?? { id: r.id, title: r.title, status: "skip", detail: "not reached" });
426
+ const count = (s) => rules.filter((r) => r.status === s).length;
427
+ return { ok: count("fail") === 0, passed: count("pass"), failed: count("fail"), skipped: count("skip"), rules, ...version ? { version } : {} };
428
+ }
429
+ }
430
+
431
+ // src/token-exchange.ts
432
+ var TokenExchangeError = class extends Error {
433
+ constructor(error, status, description) {
434
+ super(description ? `${error}: ${description}` : error);
435
+ this.error = error;
436
+ this.status = status;
437
+ this.name = "TokenExchangeError";
438
+ }
439
+ };
440
+ async function exchangeOperatorToken(input) {
441
+ const base = (input.baseUrl ?? "https://api.cookiemunch.net").replace(/\/+$/, "");
442
+ const form = new URLSearchParams({
443
+ grant_type: "urn:ietf:params:oauth:grant-type:token-exchange",
444
+ subject_token: input.subjectToken,
445
+ subject_token_type: `urn:ietf:params:oauth:token-type:${input.subjectTokenType ?? "access_token"}`,
446
+ scope: input.scope.join(" "),
447
+ resource: input.resource,
448
+ audience: "cookiemunch",
449
+ ...input.expiresIn !== void 0 ? { expires_in: String(input.expiresIn) } : {}
450
+ });
451
+ const res = await (input.fetch ?? fetch)(`${base}/v1/oauth/token`, {
452
+ method: "POST",
453
+ headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" },
454
+ body: form.toString()
455
+ });
456
+ const body = await res.json().catch(() => ({}));
457
+ if (!res.ok) {
458
+ throw new TokenExchangeError(typeof body.error === "string" ? body.error : `http_${res.status}`, res.status, typeof body.error_description === "string" ? body.error_description : void 0);
459
+ }
460
+ return {
461
+ accessToken: String(body.access_token),
462
+ expiresIn: Number(body.expires_in),
463
+ scope: String(body.scope ?? "").split(" ").filter(Boolean)
464
+ };
465
+ }
466
+
167
467
  // src/index.ts
168
- var SDK_VERSION = "0.5.1";
468
+ var SDK_VERSION = "0.7.0";
169
469
  var CookieMunchApiError = class extends Error {
170
470
  constructor(status, message, code, body) {
171
471
  super(message);
@@ -176,6 +476,10 @@ var CookieMunchApiError = class extends Error {
176
476
  }
177
477
  };
178
478
  var withBase = (doc, opts) => opts?.baseUpdatedAt !== void 0 ? { ...doc, baseUpdatedAt: opts.baseUpdatedAt } : doc;
479
+ function metaQs(o) {
480
+ const parts = Object.entries(o?.metadata ?? {}).map(([k, v]) => `metadata%5B${encodeURIComponent(k)}%5D=${encodeURIComponent(v)}`);
481
+ return parts.length ? `?${parts.join("&")}` : "";
482
+ }
179
483
  function qs(query) {
180
484
  if (!query) return "";
181
485
  const parts = [];
@@ -191,9 +495,12 @@ function createCookieMunch(opts) {
191
495
  const base = `${opts.baseUrl.replace(/\/+$/, "")}/v1`;
192
496
  const doFetch = opts.fetch ?? globalThis.fetch;
193
497
  if (!doFetch) throw new Error("No fetch available; pass { fetch } in CookieMunchOptions.");
194
- async function request(method, path, body, raw = false) {
195
- const headers = { Authorization: `Bearer ${opts.apiKey}`, "User-Agent": opts.userAgent ?? `cookiemunch-js/${SDK_VERSION}` };
498
+ async function request(method, path, body, raw = false, extraHeaders = {}) {
499
+ const headers = { Authorization: `Bearer ${opts.apiKey}`, "User-Agent": opts.userAgent ?? `cookiemunch-js/${SDK_VERSION}`, ...extraHeaders };
196
500
  if (opts.strictUnknownFields && method !== "GET") headers["Prefer"] = "strict-unknown-fields";
501
+ if (opts.apiVersion) headers["CookieMunch-Version"] = opts.apiVersion;
502
+ if (opts.secretRecipient && method !== "GET") headers["X-Secret-Recipient"] = opts.secretRecipient;
503
+ for (const [k, v] of Object.entries(opts.headers ?? {})) headers[k] = v;
197
504
  const init = { method, headers };
198
505
  if (body !== void 0) {
199
506
  headers["Content-Type"] = "application/json";
@@ -219,22 +526,49 @@ function createCookieMunch(opts) {
219
526
  return contentType.includes("application/json") ? res.json() : res.text();
220
527
  }
221
528
  const get = (p) => request("GET", p);
529
+ async function requestBytes(path) {
530
+ const res = await doFetch(`${base}${path}`, { method: "GET", headers: { Authorization: `Bearer ${opts.apiKey}`, "User-Agent": opts.userAgent ?? `cookiemunch-js/${SDK_VERSION}` } });
531
+ if (!res.ok) {
532
+ let message = `request failed with status ${res.status}`;
533
+ let code;
534
+ try {
535
+ const data = await res.json();
536
+ if (typeof data.error === "string") message = data.error;
537
+ if (typeof data.code === "string") code = data.code;
538
+ } catch {
539
+ }
540
+ throw new CookieMunchApiError(res.status, message, code);
541
+ }
542
+ return res.arrayBuffer();
543
+ }
222
544
  const enc = encodeURIComponent;
223
545
  return {
224
546
  me: () => get("/me"),
225
547
  languages: () => get("/languages"),
226
548
  orgs: {
227
- list: () => get("/orgs"),
228
- create: (name, opts2) => request("POST", "/orgs", { name, ...opts2 ?? {} })
549
+ list: (o) => get(`/orgs${metaQs(o)}`),
550
+ create: (name, opts2) => request("POST", "/orgs", { name, ...opts2 ?? {} }),
551
+ deleteSandbox: (id) => request("DELETE", `/orgs/${enc(id)}?hard=1`)
552
+ },
553
+ metadata: {
554
+ get: (type, id) => get(`/metadata/${enc(type)}/${enc(id)}`),
555
+ update: (type, id, metadata, o) => request("PATCH", `/metadata/${enc(type)}/${enc(id)}${qs({ dryRun: o?.dryRun ? "1" : void 0 })}`, { metadata })
556
+ },
557
+ partnerLinks: {
558
+ list: (o) => get(`/partner_links${metaQs(o)}`),
559
+ create: (input) => request("POST", "/partner_links", input),
560
+ revoke: (id) => request("DELETE", `/partner_links/${enc(id)}`)
229
561
  },
562
+ provisionEnv: (ref) => get(`/provision/env${qs({ ref })}`),
230
563
  sites: {
231
- list: () => get("/sites"),
564
+ list: (o) => get(`/sites${metaQs(o)}`),
232
565
  create: (input) => request("POST", "/sites", input),
233
566
  get: (cbid) => get(`/sites/${enc(cbid)}`),
234
567
  update: (cbid, patch, o) => request("PATCH", `/sites/${enc(cbid)}${qs({ dryRun: o?.dryRun ? "1" : void 0 })}`, patch),
235
568
  delete: async (cbid) => {
236
569
  await request("DELETE", `/sites/${enc(cbid)}`);
237
570
  },
571
+ deleteSandbox: (cbid) => request("DELETE", `/sites/${enc(cbid)}?hard=1`),
238
572
  listPartners: (cbid) => get(`/sites/${enc(cbid)}/partners`),
239
573
  linkPartner: (cbid, input, o) => request("POST", `/sites/${enc(cbid)}/partners${qs({ dryRun: o?.dryRun ? "1" : void 0 })}`, input),
240
574
  unlinkPartner: (cbid, linkId) => request("DELETE", `/sites/${enc(cbid)}/partners/${enc(linkId)}`),
@@ -283,6 +617,9 @@ function createCookieMunch(opts) {
283
617
  }),
284
618
  cookiePresets: (cbid) => get(`/sites/${enc(cbid)}/cookies/presets`),
285
619
  applyCookiePreset: (cbid, vendor, params, o) => request("PUT", `/sites/${enc(cbid)}/cookies/presets/${enc(vendor)}${qs({ dryRun: o?.dryRun ? "1" : void 0 })}`, params ?? {}),
620
+ importCookieManifest: (cbid, input, o) => request("PUT", `/sites/${enc(cbid)}/cookies/presets/custom${qs({ dryRun: o?.dryRun ? "1" : void 0 })}`, input),
621
+ cookieSuggestions: (cbid) => get(`/sites/${enc(cbid)}/cookies/suggestions`),
622
+ declareCookieSuggestions: (cbid, input, o) => request("POST", `/sites/${enc(cbid)}/cookies/suggestions/declare${qs({ dryRun: o?.dryRun ? "1" : void 0 })}`, input ?? {}),
286
623
  removeCookiePreset: (cbid, vendor, o) => request("DELETE", `/sites/${enc(cbid)}/cookies/presets/${enc(vendor)}${qs({ dryRun: o?.dryRun ? "1" : void 0 })}`),
287
624
  deleteObservedCookie: (async (cbid, name, o) => {
288
625
  const out = await request("DELETE", `/sites/${enc(cbid)}/observed-cookies/${enc(name)}${qs({ dryRun: o?.dryRun ? "1" : void 0 })}`);
@@ -300,6 +637,8 @@ function createCookieMunch(opts) {
300
637
  verifyChallenge: (cbid) => get(`/sites/${enc(cbid)}/verify/challenge`),
301
638
  createBulk: (sites) => request("POST", "/sites/bulk", { sites }),
302
639
  brand: (cbid) => request("POST", `/sites/${enc(cbid)}/brand`, {}),
640
+ getBrand: (cbid) => get(`/sites/${enc(cbid)}/brand`),
641
+ setBrand: (cbid, input, o) => request("PUT", `/sites/${enc(cbid)}/brand${qs({ dryRun: o?.dryRun ? "1" : void 0 })}`, input),
303
642
  getFlow: (cbid) => get(`/sites/${enc(cbid)}/flow`),
304
643
  editFlow: (cbid, operations) => request("POST", `/sites/${enc(cbid)}/flow/ops`, { operations }),
305
644
  setFlow: (cbid, config) => request("PUT", `/sites/${enc(cbid)}/flow`, config)
@@ -307,11 +646,13 @@ function createCookieMunch(opts) {
307
646
  consent: {
308
647
  verify: (cbid) => get(`/sites/${enc(cbid)}/consent/verify`),
309
648
  stats: (cbid, query) => get(`/sites/${enc(cbid)}/consent/stats${qs(toQuery(query))}`),
649
+ insights: (cbid, query) => get(`/sites/${enc(cbid)}/consent/insights${qs(toQuery(query))}`),
310
650
  log: (cbid, query) => get(`/sites/${enc(cbid)}/consent/log${qs(toQuery(query))}`),
311
651
  export: (cbid, query) => request("GET", `/sites/${enc(cbid)}/consent/export${qs(toQuery(query))}`, void 0, true),
312
652
  receipt: (cbid, stamp) => get(`/sites/${enc(cbid)}/receipt/${enc(stamp)}`),
313
653
  eraseSubject: (cbid, stamp) => request("POST", `/sites/${enc(cbid)}/erase-consent`, { stamp }),
314
654
  exportSubject: (cbid, stamp) => request("GET", `/sites/${enc(cbid)}/subject-export?stamp=${enc(stamp)}`),
655
+ categoryRegistry: () => get("/categories/registry"),
315
656
  ingest: async (input) => {
316
657
  const res = await doFetch(`${opts.baseUrl.replace(/\/+$/, "")}/api/v1/consent`, {
317
658
  method: "POST",
@@ -342,7 +683,16 @@ function createCookieMunch(opts) {
342
683
  delete: async (id) => {
343
684
  await request("DELETE", `/dsar/${enc(id)}`);
344
685
  },
345
- markTest: (id) => request("POST", `/dsar/${enc(id)}/mark-test`)
686
+ markTest: (id) => request("POST", `/dsar/${enc(id)}/mark-test`),
687
+ listByTrace: (trace) => get(`/dsar${qs({ trace })}`),
688
+ openCase: (input, o) => request("POST", "/dsar/requests", input, false, {
689
+ ...o?.idempotencyKey ? { "Idempotency-Key": o.idempotencyKey } : {},
690
+ ...o?.trace ? { "X-ForgeDeck-Trace": o.trace } : {}
691
+ }),
692
+ getCase: (id) => get(`/dsar/requests/${enc(id)}`),
693
+ cancelCase: (id) => request("POST", `/dsar/requests/${enc(id)}/cancel`),
694
+ caseReceipt: (id) => get(`/dsar/requests/${enc(id)}/receipt`),
695
+ caseReceiptPdf: (id) => requestBytes(`/dsar/requests/${enc(id)}/receipt?format=pdf`)
346
696
  },
347
697
  vendors: {
348
698
  list: () => get("/vendors"),
@@ -445,7 +795,8 @@ function createCookieMunch(opts) {
445
795
  status: (requestId) => get(`/dsar/${enc(requestId)}/fulfillment`),
446
796
  pendingTasks: (limit) => get(`/dsar/agent/tasks${qs({ limit })}`),
447
797
  reportTask: (taskId, ok, error) => request("POST", `/dsar/agent/tasks/${enc(taskId)}/result`, { ok, ...error !== void 0 ? { error } : {} }),
448
- executors: () => get("/dsar/executors"),
798
+ executors: (o) => get(`/dsar/executors${metaQs(o)}`),
799
+ executor: (id) => get(`/dsar/executors/${enc(id)}`),
449
800
  connectExecutor: (input) => request("POST", "/dsar/executors", input),
450
801
  updateExecutor: (id, patch) => request("PATCH", `/dsar/executors/${enc(id)}`, patch),
451
802
  dryRunConnectExecutor: (input) => request("POST", "/dsar/executors?dryRun=1", input),
@@ -460,7 +811,11 @@ function createCookieMunch(opts) {
460
811
  },
461
812
  taskExport: (requestId, taskId) => get(`/dsar/${enc(requestId)}/tasks/${enc(taskId)}/export`),
462
813
  task: (requestId, taskId) => get(`/dsar/${enc(requestId)}/tasks/${enc(taskId)}`),
463
- testExecutor: (id, input) => request("POST", `/dsar/executors/${enc(id)}/test`, input)
814
+ testExecutor: (id, input) => request("POST", `/dsar/executors/${enc(id)}/test`, input),
815
+ recheckExecutor: (id) => request("POST", `/dsar/executors/${enc(id)}/recheck`),
816
+ executorHealthHistory: (id, opts2 = {}) => get(`/dsar/executors/${enc(id)}/health/history${qs({ limit: opts2.limit })}`),
817
+ executorConformance: (id) => request("POST", `/dsar/executors/${enc(id)}/test`, { conformance: true }),
818
+ rollExecutorCompletionSecret: (id) => request("POST", `/dsar/executors/${enc(id)}/completion-secret/roll`)
464
819
  },
465
820
  preferences: {
466
821
  list: () => get("/preferences"),
@@ -484,11 +839,32 @@ function createCookieMunch(opts) {
484
839
  },
485
840
  roll: (prefix) => request("POST", `/keys/${enc(prefix)}/roll`),
486
841
  update: (prefix, patch) => request("PATCH", `/keys/${enc(prefix)}`, patch),
487
- report: (o) => get(`/keys/report${qs({ unusedDays: o?.unusedDays, expiringDays: o?.expiringDays })}`)
842
+ report: (o) => get(`/keys/report${qs({ unusedDays: o?.unusedDays, expiringDays: o?.expiringDays })}`),
843
+ listStandard: () => get("/keys?shape=standard"),
844
+ self: () => get("/keys/self"),
845
+ updateSelf: (input) => request("PATCH", "/keys/self", input),
846
+ rollSelf: () => request("POST", "/keys/self/roll"),
847
+ vend: (input) => request("POST", "/keys/vend", input),
848
+ revokeLeaked: (report) => request("POST", "/keys/revoke_leaked", report)
488
849
  },
489
850
  usage: () => get("/usage"),
851
+ entitlements: () => get("/entitlements"),
852
+ versions: () => get("/versions"),
853
+ sandbox: {
854
+ get: async () => (await get("/sandbox")).sandbox,
855
+ create: (input) => request("POST", "/sandbox", input ?? {}),
856
+ delete: () => request("DELETE", "/sandbox")
857
+ },
490
858
  integrationHealth: (cbid) => get(`/integration-health${qs({ cbid })}`),
491
- provision: (input, o) => request("POST", `/provision${qs({ dryRun: o?.dryRun ? "1" : void 0 })}`, input),
859
+ healthWaivers: {
860
+ list: (cbid) => get(`/integration-health/waivers${qs({ cbid })}`),
861
+ set: (input) => request("PUT", "/integration-health/waivers", input),
862
+ remove: async (check, opts2) => {
863
+ await request("DELETE", `/integration-health/waivers${qs({ check, cbid: opts2?.cbid, subject: opts2?.subject })}`);
864
+ }
865
+ },
866
+ embedManifest: (cbid) => get(`/embed${qs({ cbid })}`),
867
+ provision: (input, o) => request("POST", `/provision${qs({ dryRun: o?.dryRun ? "1" : void 0, fromStep: o?.fromStep })}`, input),
492
868
  org: {
493
869
  get: () => get("/org"),
494
870
  update: (patch) => request("PATCH", "/org", patch),
@@ -518,7 +894,14 @@ function createCookieMunch(opts) {
518
894
  remove: () => request("DELETE", "/org/rights-identity")
519
895
  }
520
896
  },
521
- audit: (limit) => get(`/audit${qs({ limit })}`),
897
+ audit: (limit, opts2) => get(`/audit${qs({ limit, actor: opts2?.actor })}`),
898
+ auditSink: {
899
+ get: () => get("/audit/sink"),
900
+ set: (input) => request("PUT", "/audit/sink", input),
901
+ remove: async () => {
902
+ await request("DELETE", "/audit/sink");
903
+ }
904
+ },
522
905
  assets: {
523
906
  upload: (input) => request("POST", "/assets", input),
524
907
  delete: async (urlOrFileName) => {
@@ -541,7 +924,7 @@ function createCookieMunch(opts) {
541
924
  }
542
925
  },
543
926
  webhooks: {
544
- list: () => get("/webhooks"),
927
+ list: (o) => get(`/webhooks${metaQs(o)}`),
545
928
  create: (input) => request("POST", "/webhooks", input),
546
929
  update: (id, patch) => request("PATCH", `/webhooks/${enc(id)}`, patch),
547
930
  delete: async (id) => {
@@ -550,7 +933,8 @@ function createCookieMunch(opts) {
550
933
  rollSecret: (id) => request("POST", `/webhooks/${enc(id)}/roll`),
551
934
  test: (id) => request("POST", `/webhooks/${enc(id)}/test`),
552
935
  deadLetters: () => get("/webhooks/dead-letters"),
553
- replayDeadLetter: (id) => request("POST", `/webhooks/dead-letters/${enc(id)}/replay`)
936
+ replayDeadLetter: (id) => request("POST", `/webhooks/dead-letters/${enc(id)}/replay`),
937
+ replay: (id, since, opts2) => request("POST", `/webhooks/${enc(id)}/replay${qs({ since: String(since), limit: opts2?.limit })}`)
554
938
  },
555
939
  rightsLinks: {
556
940
  create: (cbid, input) => request("POST", `/sites/${enc(cbid)}/rights-links`, input)
@@ -570,6 +954,13 @@ function createCookieMunch(opts) {
570
954
  set: (cbid, input) => request("PUT", `/sites/${enc(cbid)}/rights-identity`, input),
571
955
  remove: (cbid) => request("DELETE", `/sites/${enc(cbid)}/rights-identity`)
572
956
  },
957
+ dsarSettings: {
958
+ get: (cbid) => get(`/sites/${enc(cbid)}/dsar-settings`),
959
+ set: (cbid, input) => request("PUT", `/sites/${enc(cbid)}/dsar-settings`, input)
960
+ },
961
+ integrations: {
962
+ subjectCard: (app, person) => get(`/integrations/${enc(app)}/card${qs(person)}`)
963
+ },
573
964
  subjects: {
574
965
  consent: (subjectId) => get(`/subjects/${enc(subjectId)}/consent`)
575
966
  },
@@ -589,15 +980,25 @@ function createCookieMunch(opts) {
589
980
  }
590
981
  // Annotate the CommonJS export names for ESM import in node:
591
982
  0 && (module.exports = {
983
+ CANARY_ISSUER,
984
+ CONFORMANCE_RULES,
592
985
  CONSENT_COOKIE_NAME,
593
986
  CookieMunchApiError,
987
+ DEFAULT_JWKS_URL,
594
988
  SDK_VERSION,
989
+ TokenExchangeError,
990
+ canaryIdentityFor,
991
+ consentSignaturePayload,
595
992
  createCookieMunch,
993
+ exchangeOperatorToken,
596
994
  getConsent,
597
995
  isAllowed,
598
996
  mintConsentSyncToken,
599
997
  parseConsentValue,
600
998
  readConsent,
999
+ runExecutorConformance,
1000
+ verifyDecisionSignature,
1001
+ verifyToken,
601
1002
  verifyWebhookSignature
602
1003
  });
603
1004
  //# sourceMappingURL=index.cjs.map