@cookiemunch/sdk 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -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 +368 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1163 -41
- package/dist/index.d.ts +1163 -41
- package/dist/index.js +287 -17
- 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/dist/index.cjs
CHANGED
|
@@ -20,15 +20,23 @@ 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
|
+
canaryIdentityFor: () => canaryIdentityFor,
|
|
30
|
+
consentSignaturePayload: () => consentSignaturePayload,
|
|
26
31
|
createCookieMunch: () => createCookieMunch,
|
|
27
32
|
getConsent: () => getConsent,
|
|
28
33
|
isAllowed: () => isAllowed,
|
|
29
34
|
mintConsentSyncToken: () => mintConsentSyncToken,
|
|
30
35
|
parseConsentValue: () => parseConsentValue,
|
|
31
36
|
readConsent: () => readConsent,
|
|
37
|
+
runExecutorConformance: () => runExecutorConformance,
|
|
38
|
+
verifyDecisionSignature: () => verifyDecisionSignature,
|
|
39
|
+
verifyToken: () => verifyToken,
|
|
32
40
|
verifyWebhookSignature: () => verifyWebhookSignature
|
|
33
41
|
});
|
|
34
42
|
module.exports = __toCommonJS(src_exports);
|
|
@@ -89,6 +97,7 @@ function normalize(o) {
|
|
|
89
97
|
for (const [k, v] of Object.entries(r.custom)) if (bool(v)) custom[k] = v;
|
|
90
98
|
if (Object.keys(custom).length > 0) out.custom = custom;
|
|
91
99
|
}
|
|
100
|
+
if (typeof r.sig === "string") out.sig = r.sig;
|
|
92
101
|
return out;
|
|
93
102
|
}
|
|
94
103
|
function parseConsentValue(raw) {
|
|
@@ -108,6 +117,12 @@ function parseConsentValue(raw) {
|
|
|
108
117
|
}
|
|
109
118
|
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
110
119
|
function readConsent(source, opts = {}) {
|
|
120
|
+
const consent = readUnsigned(source, opts);
|
|
121
|
+
if (opts.requireSigned !== true) return consent;
|
|
122
|
+
if (!consent?.sig) return Promise.resolve(null);
|
|
123
|
+
return verifyDecisionSignature(consent, opts).then((ok) => ok ? consent : null);
|
|
124
|
+
}
|
|
125
|
+
function readUnsigned(source, opts) {
|
|
111
126
|
const raw = cookieValue(cookieHeaderOf(source), opts.cookieName ?? CONSENT_COOKIE_NAME);
|
|
112
127
|
const consent = raw ? parseConsentValue(raw) : null;
|
|
113
128
|
if (!consent) return null;
|
|
@@ -128,6 +143,78 @@ function isAllowed(consent, category) {
|
|
|
128
143
|
if (category === "marketing") return consent.marketing === true;
|
|
129
144
|
return consent.custom?.[category] === true;
|
|
130
145
|
}
|
|
146
|
+
var DEFAULT_JWKS_URL = "https://api.cookiemunch.net/v1/partner/jwks.json";
|
|
147
|
+
function consentSignaturePayload(d) {
|
|
148
|
+
const b = (v) => v ? "1" : "0";
|
|
149
|
+
const custom = d.custom ?? {};
|
|
150
|
+
const named = Object.keys(custom).sort().map((k) => `${k}=${b(custom[k])}`).join(",");
|
|
151
|
+
return `cmc1|${d.stamp}|${d.ver}|${d.utc}|${b(d.preferences)}${b(d.statistics)}${b(d.marketing)}|${named}`;
|
|
152
|
+
}
|
|
153
|
+
var jwksCache = /* @__PURE__ */ new Map();
|
|
154
|
+
var JWKS_TTL_MS = 60 * 60 * 1e3;
|
|
155
|
+
async function keySet(opts, kid) {
|
|
156
|
+
if (opts.jwks) return opts.jwks;
|
|
157
|
+
const url = opts.jwksUrl ?? DEFAULT_JWKS_URL;
|
|
158
|
+
const hit = jwksCache.get(url);
|
|
159
|
+
if (hit && Date.now() - hit.at < JWKS_TTL_MS && hit.set.keys.some((k) => k.kid === kid)) return hit.set;
|
|
160
|
+
const f = opts.fetch ?? globalThis.fetch;
|
|
161
|
+
if (!f) throw new Error("no fetch available to load the key set; pass `jwks`");
|
|
162
|
+
const res = await f(url);
|
|
163
|
+
if (!res.ok) throw new Error(`key set ${url} answered ${res.status}`);
|
|
164
|
+
const set = await res.json();
|
|
165
|
+
if (!set || !Array.isArray(set.keys)) throw new Error(`key set ${url} is not a JWKS`);
|
|
166
|
+
jwksCache.set(url, { at: Date.now(), set });
|
|
167
|
+
return set;
|
|
168
|
+
}
|
|
169
|
+
function b64urlBytes(s) {
|
|
170
|
+
const b64 = s.replace(/-/g, "+").replace(/_/g, "/") + "===".slice((s.length + 3) % 4);
|
|
171
|
+
const bin = atob(b64);
|
|
172
|
+
const out = new Uint8Array(bin.length);
|
|
173
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
174
|
+
return out;
|
|
175
|
+
}
|
|
176
|
+
async function edVerify(jwk, data, signature) {
|
|
177
|
+
const subtle = globalThis.crypto?.subtle;
|
|
178
|
+
if (!subtle) throw new Error("WebCrypto is unavailable in this runtime");
|
|
179
|
+
if (jwk.kty !== "OKP" || jwk.crv !== "Ed25519" || !jwk.x) return false;
|
|
180
|
+
const key = await subtle.importKey("jwk", { kty: "OKP", crv: "Ed25519", x: jwk.x }, { name: "Ed25519" }, false, ["verify"]);
|
|
181
|
+
return subtle.verify({ name: "Ed25519" }, key, b64urlBytes(signature), new TextEncoder().encode(data));
|
|
182
|
+
}
|
|
183
|
+
async function verifyDecisionSignature(consent, opts = {}) {
|
|
184
|
+
const dot = consent.sig?.lastIndexOf(".") ?? -1;
|
|
185
|
+
if (!consent.sig || dot <= 0) return false;
|
|
186
|
+
const kid = consent.sig.slice(0, dot);
|
|
187
|
+
try {
|
|
188
|
+
const jwk = (await keySet(opts, kid)).keys.find((k) => k.kid === kid);
|
|
189
|
+
if (!jwk) return false;
|
|
190
|
+
return await edVerify(jwk, consentSignaturePayload(consent), consent.sig.slice(dot + 1));
|
|
191
|
+
} catch {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
async function verifyToken(token, opts = {}) {
|
|
196
|
+
const parts = typeof token === "string" ? token.split(".") : [];
|
|
197
|
+
if (parts.length !== 3) throw new Error("not a JWT");
|
|
198
|
+
const [h, p, s] = parts;
|
|
199
|
+
let header;
|
|
200
|
+
let claims;
|
|
201
|
+
try {
|
|
202
|
+
header = JSON.parse(new TextDecoder().decode(b64urlBytes(h)));
|
|
203
|
+
claims = JSON.parse(new TextDecoder().decode(b64urlBytes(p)));
|
|
204
|
+
} catch {
|
|
205
|
+
throw new Error("malformed token");
|
|
206
|
+
}
|
|
207
|
+
if (header.alg !== "EdDSA" || !header.kid) throw new Error("unexpected token algorithm");
|
|
208
|
+
const jwk = (await keySet(opts, header.kid)).keys.find((k) => k.kid === header.kid);
|
|
209
|
+
if (!jwk || !await edVerify(jwk, `${h}.${p}`, s)) throw new Error("bad signature");
|
|
210
|
+
if (claims.token_use !== "consent") throw new Error("not a consent token");
|
|
211
|
+
const now = (opts.now ?? Date.now()) / 1e3;
|
|
212
|
+
const leeway = opts.leewaySeconds ?? 30;
|
|
213
|
+
if (typeof claims.exp !== "number" || now > claims.exp + leeway) throw new Error("expired");
|
|
214
|
+
if (typeof claims.iat === "number" && claims.iat > now + leeway) throw new Error("issued in the future");
|
|
215
|
+
if (opts.cbid !== void 0 && (claims.cbid !== opts.cbid || claims.aud !== `cookiemunch:${opts.cbid}`)) throw new Error("token is for another site");
|
|
216
|
+
return claims;
|
|
217
|
+
}
|
|
131
218
|
|
|
132
219
|
// src/webhooks.ts
|
|
133
220
|
var import_node_crypto = require("crypto");
|
|
@@ -164,8 +251,183 @@ function mintConsentSyncToken(input) {
|
|
|
164
251
|
return `cms1.${payload}.${sig}`;
|
|
165
252
|
}
|
|
166
253
|
|
|
254
|
+
// src/executor-conformance.ts
|
|
255
|
+
var CANARY_ISSUER = "https://canary.cookiemunch.net";
|
|
256
|
+
var CANARY_EMAIL_RE = /^canary-([0-9a-f]{16})@canary\.cookiemunch\.net$/;
|
|
257
|
+
function canaryIdentityFor(canaryEmail) {
|
|
258
|
+
const m = CANARY_EMAIL_RE.exec(canaryEmail.trim().toLowerCase());
|
|
259
|
+
if (!m) throw new Error("canaryEmail must be the canary address canary-<16 hex>@canary.cookiemunch.net");
|
|
260
|
+
return { iss: CANARY_ISSUER, sub: `canary_${m[1]}`, ext: `canary:${m[1]}` };
|
|
261
|
+
}
|
|
262
|
+
var CONFORMANCE_RULES = [
|
|
263
|
+
{ id: "auth.required", title: "A request without the credential is refused (401 or 403)." },
|
|
264
|
+
{ id: "lookup.email.found", title: "The canary subject is found by its email." },
|
|
265
|
+
{ id: "lookup.identity.found", title: "The canary subject is found by its identity alone (iss + sub), and it is the same subject." },
|
|
266
|
+
{ id: "lookup.unknown.no_match", title: 'An address nobody has answers { subjectId: null, status: "no_match" }.' },
|
|
267
|
+
{ id: "lookup.identity_beats_email", title: "An identity that matches no one is never matched through the email of someone else." },
|
|
268
|
+
{ id: "requests.bad_body.400", title: "A request with an unknown type answers 400 { error, field }." },
|
|
269
|
+
{ id: "requests.alias.canonical_type", title: 'An alias (erasure) is accepted and answered with canonicalType "deletion".' },
|
|
270
|
+
{ 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.' },
|
|
271
|
+
{ id: "requests.idempotency.replay", title: "The same Idempotency-Key with the same body returns the same request, not a second one." },
|
|
272
|
+
{ id: "requests.idempotency.mismatch_409", title: "The same Idempotency-Key with a different body answers 409." },
|
|
273
|
+
{ id: "requests.get", title: "GET /privacy/requests/{id} returns the request, still scheduled." },
|
|
274
|
+
{ id: "requests.unknown.404", title: "An unknown request id answers 404." },
|
|
275
|
+
{ id: "requests.cancel.before_execute_after", title: 'A cancel before executeAfter answers status "cancelled".' },
|
|
276
|
+
{ id: "requests.cancel.keeps_data", title: "After a cancel the subject is still there: a lookup finds it." },
|
|
277
|
+
{ id: "version.header", title: "Answers carry X-Version (the highest contract version the executor supports)." }
|
|
278
|
+
];
|
|
279
|
+
function randomTag() {
|
|
280
|
+
const bytes = new Uint8Array(8);
|
|
281
|
+
const c = globalThis.crypto;
|
|
282
|
+
if (c?.getRandomValues) c.getRandomValues(bytes);
|
|
283
|
+
else for (let i = 0; i < bytes.length; i++) bytes[i] = Math.floor(Math.random() * 256);
|
|
284
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
285
|
+
}
|
|
286
|
+
function field(json, key) {
|
|
287
|
+
return json && typeof json === "object" ? json[key] : void 0;
|
|
288
|
+
}
|
|
289
|
+
async function runExecutorConformance(o) {
|
|
290
|
+
const doFetch = o.fetch ?? fetch;
|
|
291
|
+
const now = o.now ?? (() => Date.now());
|
|
292
|
+
const base = o.baseUrl.replace(/\/+$/, "") + (o.prefix ?? "/privacy");
|
|
293
|
+
let url;
|
|
294
|
+
try {
|
|
295
|
+
url = new URL(base);
|
|
296
|
+
} catch {
|
|
297
|
+
throw new Error("baseUrl must be an absolute URL");
|
|
298
|
+
}
|
|
299
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && /^(localhost|127\.0\.0\.1|\[::1\])$/.test(url.hostname))) {
|
|
300
|
+
throw new Error("baseUrl must be https (http only for localhost)");
|
|
301
|
+
}
|
|
302
|
+
const identity = canaryIdentityFor(o.canaryEmail);
|
|
303
|
+
const canaryEmail = o.canaryEmail.trim().toLowerCase();
|
|
304
|
+
const auth = o.auth ?? { header: "Authorization", format: "Bearer {secret}" };
|
|
305
|
+
const run = o.runId ?? randomTag();
|
|
306
|
+
const results = /* @__PURE__ */ new Map();
|
|
307
|
+
let version;
|
|
308
|
+
const set = (id, status, detail) => {
|
|
309
|
+
const rule = CONFORMANCE_RULES.find((r) => r.id === id);
|
|
310
|
+
results.set(id, { id, title: rule.title, status, ...detail ? { detail } : {} });
|
|
311
|
+
};
|
|
312
|
+
const pass = (id) => set(id, "pass");
|
|
313
|
+
const fail = (id, detail) => set(id, "fail", detail);
|
|
314
|
+
const skip = (id, detail) => set(id, "skip", detail);
|
|
315
|
+
async function call(method, path, body, extra = {}, withAuth = true) {
|
|
316
|
+
const headers = {
|
|
317
|
+
accept: "application/json",
|
|
318
|
+
"X-Version": "2",
|
|
319
|
+
...withAuth ? { [auth.header]: auth.format.replace("{secret}", o.secret) } : {},
|
|
320
|
+
...body !== void 0 ? { "content-type": "application/json" } : {},
|
|
321
|
+
...extra
|
|
322
|
+
};
|
|
323
|
+
try {
|
|
324
|
+
const res = await doFetch(base + path, { method, headers, ...body !== void 0 ? { body: JSON.stringify(body) } : {} });
|
|
325
|
+
const text = await res.text().catch(() => "");
|
|
326
|
+
let json;
|
|
327
|
+
try {
|
|
328
|
+
json = text ? JSON.parse(text) : void 0;
|
|
329
|
+
} catch {
|
|
330
|
+
json = void 0;
|
|
331
|
+
}
|
|
332
|
+
const v = res.headers.get("x-version");
|
|
333
|
+
if (v && !version) version = v;
|
|
334
|
+
return { status: res.status, json, version: v };
|
|
335
|
+
} catch (err) {
|
|
336
|
+
return { status: 0, json: void 0, version: null, error: err instanceof Error ? err.message : String(err) };
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
const said = (a) => a.status === 0 ? `unreachable: ${a.error}` : `HTTP ${a.status} ${JSON.stringify(a.json ?? null).slice(0, 200)}`;
|
|
340
|
+
const key = (rule) => `conformance:${run}:${rule}`;
|
|
341
|
+
const anon = await call("POST", "/lookup", { emails: [canaryEmail] }, {}, false);
|
|
342
|
+
if (anon.status === 0) {
|
|
343
|
+
for (const r of CONFORMANCE_RULES) skip(r.id, said(anon));
|
|
344
|
+
fail("auth.required", said(anon));
|
|
345
|
+
return report();
|
|
346
|
+
}
|
|
347
|
+
if (anon.status === 401 || anon.status === 403) pass("auth.required");
|
|
348
|
+
else fail("auth.required", `without a credential: ${said(anon)}`);
|
|
349
|
+
const byEmail = await call("POST", "/lookup", { emails: [canaryEmail] }, { "Idempotency-Key": key("lookup-email") });
|
|
350
|
+
const subjectId = field(byEmail.json, "subjectId");
|
|
351
|
+
if (byEmail.status === 200 && field(byEmail.json, "status") === "found" && typeof subjectId === "string" && subjectId) pass("lookup.email.found");
|
|
352
|
+
else fail("lookup.email.found", `the canary address: ${said(byEmail)}`);
|
|
353
|
+
const byIdentity = await call("POST", "/lookup", { emails: [], identity, atlas: identity }, { "Idempotency-Key": key("lookup-identity") });
|
|
354
|
+
if (byIdentity.status === 200 && field(byIdentity.json, "status") === "found" && field(byIdentity.json, "subjectId") === subjectId && typeof subjectId === "string") {
|
|
355
|
+
pass("lookup.identity.found");
|
|
356
|
+
} else {
|
|
357
|
+
fail("lookup.identity.found", `the canary identity ${identity.sub}: ${said(byIdentity)}${typeof subjectId === "string" ? ` (by email it was ${subjectId})` : ""}`);
|
|
358
|
+
}
|
|
359
|
+
const nobody = await call("POST", "/lookup", { emails: [`nobody-${run}@canary.cookiemunch.net`] }, { "Idempotency-Key": key("lookup-nobody") });
|
|
360
|
+
if (nobody.status === 200 && field(nobody.json, "status") === "no_match" && field(nobody.json, "subjectId") === null) pass("lookup.unknown.no_match");
|
|
361
|
+
else fail("lookup.unknown.no_match", said(nobody));
|
|
362
|
+
const stranger = { iss: identity.iss, sub: `canary_other_${run}` };
|
|
363
|
+
const crossed = await call("POST", "/lookup", { emails: [canaryEmail], identity: stranger, atlas: stranger }, { "Idempotency-Key": key("lookup-crossed") });
|
|
364
|
+
if (crossed.status === 200 && field(crossed.json, "status") === "no_match") pass("lookup.identity_beats_email");
|
|
365
|
+
else {
|
|
366
|
+
fail(
|
|
367
|
+
"lookup.identity_beats_email",
|
|
368
|
+
`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`
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
const bad = await call("POST", "/requests", { subjectId: subjectId ?? "x", type: "teleport", caseId: `conformance-${run}` }, { "Idempotency-Key": key("bad-body") });
|
|
372
|
+
if (bad.status === 400 && typeof field(bad.json, "error") === "string" && typeof field(bad.json, "field") === "string") pass("requests.bad_body.400");
|
|
373
|
+
else fail("requests.bad_body.400", said(bad));
|
|
374
|
+
if (typeof subjectId !== "string") {
|
|
375
|
+
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"]) {
|
|
376
|
+
skip(id, "needs the canary subject, which the lookup did not find");
|
|
377
|
+
}
|
|
378
|
+
} else {
|
|
379
|
+
const executeAfter = new Date(Math.ceil((now() + 36e5) / 1e3) * 1e3).toISOString();
|
|
380
|
+
const body = { subjectId, type: "erasure", executeAfter, caseId: `conformance-${run}`, reason: "Cookie Munch conformance check (canary subject)" };
|
|
381
|
+
const opened = await call("POST", "/requests", body, { "Idempotency-Key": key("open") });
|
|
382
|
+
const id = field(opened.json, "id");
|
|
383
|
+
const st = field(opened.json, "status");
|
|
384
|
+
if ((opened.status === 200 || opened.status === 201) && field(opened.json, "canonicalType") === "deletion") pass("requests.alias.canonical_type");
|
|
385
|
+
else fail("requests.alias.canonical_type", `type "erasure": ${said(opened)}`);
|
|
386
|
+
const eta = field(opened.json, "eta");
|
|
387
|
+
if ((opened.status === 200 || opened.status === 201) && st === "scheduled" && typeof eta === "string" && Date.parse(eta) === Date.parse(executeAfter)) {
|
|
388
|
+
pass("requests.execute_after.scheduled");
|
|
389
|
+
} else if (st === "completed" || st === "completed_with_retention" || st === "processing") {
|
|
390
|
+
fail("requests.execute_after.scheduled", `executeAfter is ${executeAfter}, an hour away, and the executor answered "${String(st)}": it acted before executeAfter`);
|
|
391
|
+
} else {
|
|
392
|
+
fail("requests.execute_after.scheduled", `expected status "scheduled" with eta ${executeAfter}: ${said(opened)}`);
|
|
393
|
+
}
|
|
394
|
+
const replay = await call("POST", "/requests", body, { "Idempotency-Key": key("open") });
|
|
395
|
+
if (typeof id === "string" && (replay.status === 200 || replay.status === 201) && field(replay.json, "id") === id) pass("requests.idempotency.replay");
|
|
396
|
+
else fail("requests.idempotency.replay", `a retry with the same key: ${said(replay)}${typeof id === "string" ? ` (first answer id ${id})` : ""}`);
|
|
397
|
+
const mismatch = await call("POST", "/requests", { ...body, reason: "a different body" }, { "Idempotency-Key": key("open") });
|
|
398
|
+
if (mismatch.status === 409) pass("requests.idempotency.mismatch_409");
|
|
399
|
+
else fail("requests.idempotency.mismatch_409", `the same key with a different body: ${said(mismatch)}`);
|
|
400
|
+
if (typeof id !== "string") {
|
|
401
|
+
skip("requests.get", "no request id came back");
|
|
402
|
+
skip("requests.cancel.before_execute_after", "no request id came back");
|
|
403
|
+
skip("requests.cancel.keeps_data", "no request id came back");
|
|
404
|
+
} else {
|
|
405
|
+
const got = await call("GET", `/requests/${encodeURIComponent(id)}`, void 0, { "Idempotency-Key": key("get") });
|
|
406
|
+
if (got.status === 200 && field(got.json, "id") === id && field(got.json, "status") === "scheduled") pass("requests.get");
|
|
407
|
+
else fail("requests.get", said(got));
|
|
408
|
+
const cancelled = await call("POST", `/requests/${encodeURIComponent(id)}/cancel`, {}, { "Idempotency-Key": key("cancel") });
|
|
409
|
+
if (cancelled.status === 200 && field(cancelled.json, "status") === "cancelled") pass("requests.cancel.before_execute_after");
|
|
410
|
+
else fail("requests.cancel.before_execute_after", said(cancelled));
|
|
411
|
+
const after = await call("POST", "/lookup", { emails: [canaryEmail] }, { "Idempotency-Key": key("lookup-after-cancel") });
|
|
412
|
+
if (after.status === 200 && field(after.json, "status") === "found") pass("requests.cancel.keeps_data");
|
|
413
|
+
else fail("requests.cancel.keeps_data", `a lookup after the cancel: ${said(after)} \u2014 a cancel before executeAfter must leave the data intact`);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
const unknown = await call("GET", `/requests/conformance-missing-${run}`, void 0, { "Idempotency-Key": key("unknown") });
|
|
417
|
+
if (unknown.status === 404) pass("requests.unknown.404");
|
|
418
|
+
else fail("requests.unknown.404", said(unknown));
|
|
419
|
+
if (version) pass("version.header");
|
|
420
|
+
else fail("version.header", "no answer carried an X-Version header");
|
|
421
|
+
return report();
|
|
422
|
+
function report() {
|
|
423
|
+
const rules = CONFORMANCE_RULES.map((r) => results.get(r.id) ?? { id: r.id, title: r.title, status: "skip", detail: "not reached" });
|
|
424
|
+
const count = (s) => rules.filter((r) => r.status === s).length;
|
|
425
|
+
return { ok: count("fail") === 0, passed: count("pass"), failed: count("fail"), skipped: count("skip"), rules, ...version ? { version } : {} };
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
167
429
|
// src/index.ts
|
|
168
|
-
var SDK_VERSION = "0.
|
|
430
|
+
var SDK_VERSION = "0.6.0";
|
|
169
431
|
var CookieMunchApiError = class extends Error {
|
|
170
432
|
constructor(status, message, code, body) {
|
|
171
433
|
super(message);
|
|
@@ -176,6 +438,10 @@ var CookieMunchApiError = class extends Error {
|
|
|
176
438
|
}
|
|
177
439
|
};
|
|
178
440
|
var withBase = (doc, opts) => opts?.baseUpdatedAt !== void 0 ? { ...doc, baseUpdatedAt: opts.baseUpdatedAt } : doc;
|
|
441
|
+
function metaQs(o) {
|
|
442
|
+
const parts = Object.entries(o?.metadata ?? {}).map(([k, v]) => `metadata%5B${encodeURIComponent(k)}%5D=${encodeURIComponent(v)}`);
|
|
443
|
+
return parts.length ? `?${parts.join("&")}` : "";
|
|
444
|
+
}
|
|
179
445
|
function qs(query) {
|
|
180
446
|
if (!query) return "";
|
|
181
447
|
const parts = [];
|
|
@@ -191,9 +457,11 @@ function createCookieMunch(opts) {
|
|
|
191
457
|
const base = `${opts.baseUrl.replace(/\/+$/, "")}/v1`;
|
|
192
458
|
const doFetch = opts.fetch ?? globalThis.fetch;
|
|
193
459
|
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}
|
|
460
|
+
async function request(method, path, body, raw = false, extraHeaders = {}) {
|
|
461
|
+
const headers = { Authorization: `Bearer ${opts.apiKey}`, "User-Agent": opts.userAgent ?? `cookiemunch-js/${SDK_VERSION}`, ...extraHeaders };
|
|
196
462
|
if (opts.strictUnknownFields && method !== "GET") headers["Prefer"] = "strict-unknown-fields";
|
|
463
|
+
if (opts.secretRecipient && method !== "GET") headers["X-Secret-Recipient"] = opts.secretRecipient;
|
|
464
|
+
for (const [k, v] of Object.entries(opts.headers ?? {})) headers[k] = v;
|
|
197
465
|
const init = { method, headers };
|
|
198
466
|
if (body !== void 0) {
|
|
199
467
|
headers["Content-Type"] = "application/json";
|
|
@@ -219,22 +487,49 @@ function createCookieMunch(opts) {
|
|
|
219
487
|
return contentType.includes("application/json") ? res.json() : res.text();
|
|
220
488
|
}
|
|
221
489
|
const get = (p) => request("GET", p);
|
|
490
|
+
async function requestBytes(path) {
|
|
491
|
+
const res = await doFetch(`${base}${path}`, { method: "GET", headers: { Authorization: `Bearer ${opts.apiKey}`, "User-Agent": opts.userAgent ?? `cookiemunch-js/${SDK_VERSION}` } });
|
|
492
|
+
if (!res.ok) {
|
|
493
|
+
let message = `request failed with status ${res.status}`;
|
|
494
|
+
let code;
|
|
495
|
+
try {
|
|
496
|
+
const data = await res.json();
|
|
497
|
+
if (typeof data.error === "string") message = data.error;
|
|
498
|
+
if (typeof data.code === "string") code = data.code;
|
|
499
|
+
} catch {
|
|
500
|
+
}
|
|
501
|
+
throw new CookieMunchApiError(res.status, message, code);
|
|
502
|
+
}
|
|
503
|
+
return res.arrayBuffer();
|
|
504
|
+
}
|
|
222
505
|
const enc = encodeURIComponent;
|
|
223
506
|
return {
|
|
224
507
|
me: () => get("/me"),
|
|
225
508
|
languages: () => get("/languages"),
|
|
226
509
|
orgs: {
|
|
227
|
-
list: () => get(
|
|
228
|
-
create: (name, opts2) => request("POST", "/orgs", { name, ...opts2 ?? {} })
|
|
510
|
+
list: (o) => get(`/orgs${metaQs(o)}`),
|
|
511
|
+
create: (name, opts2) => request("POST", "/orgs", { name, ...opts2 ?? {} }),
|
|
512
|
+
deleteSandbox: (id) => request("DELETE", `/orgs/${enc(id)}?hard=1`)
|
|
513
|
+
},
|
|
514
|
+
metadata: {
|
|
515
|
+
get: (type, id) => get(`/metadata/${enc(type)}/${enc(id)}`),
|
|
516
|
+
update: (type, id, metadata, o) => request("PATCH", `/metadata/${enc(type)}/${enc(id)}${qs({ dryRun: o?.dryRun ? "1" : void 0 })}`, { metadata })
|
|
517
|
+
},
|
|
518
|
+
partnerLinks: {
|
|
519
|
+
list: (o) => get(`/partner_links${metaQs(o)}`),
|
|
520
|
+
create: (input) => request("POST", "/partner_links", input),
|
|
521
|
+
revoke: (id) => request("DELETE", `/partner_links/${enc(id)}`)
|
|
229
522
|
},
|
|
523
|
+
provisionEnv: (ref) => get(`/provision/env${qs({ ref })}`),
|
|
230
524
|
sites: {
|
|
231
|
-
list: () => get(
|
|
525
|
+
list: (o) => get(`/sites${metaQs(o)}`),
|
|
232
526
|
create: (input) => request("POST", "/sites", input),
|
|
233
527
|
get: (cbid) => get(`/sites/${enc(cbid)}`),
|
|
234
528
|
update: (cbid, patch, o) => request("PATCH", `/sites/${enc(cbid)}${qs({ dryRun: o?.dryRun ? "1" : void 0 })}`, patch),
|
|
235
529
|
delete: async (cbid) => {
|
|
236
530
|
await request("DELETE", `/sites/${enc(cbid)}`);
|
|
237
531
|
},
|
|
532
|
+
deleteSandbox: (cbid) => request("DELETE", `/sites/${enc(cbid)}?hard=1`),
|
|
238
533
|
listPartners: (cbid) => get(`/sites/${enc(cbid)}/partners`),
|
|
239
534
|
linkPartner: (cbid, input, o) => request("POST", `/sites/${enc(cbid)}/partners${qs({ dryRun: o?.dryRun ? "1" : void 0 })}`, input),
|
|
240
535
|
unlinkPartner: (cbid, linkId) => request("DELETE", `/sites/${enc(cbid)}/partners/${enc(linkId)}`),
|
|
@@ -257,7 +552,7 @@ function createCookieMunch(opts) {
|
|
|
257
552
|
const asked = Math.max(0, Math.min(6e4, left));
|
|
258
553
|
const t0 = Date.now();
|
|
259
554
|
const s = await get(`/sites/${enc(cbid)}/scan${qs({ wait: asked / 1e3 })}`);
|
|
260
|
-
if (s.status !== "scanning" || until - Date.now()
|
|
555
|
+
if (s.status !== "scanning" || until - Date.now() < 50) return s;
|
|
261
556
|
const early = Math.min(1e3, until - Date.now()) - (Date.now() - t0);
|
|
262
557
|
if (early > 0) await new Promise((r) => setTimeout(r, early));
|
|
263
558
|
}
|
|
@@ -283,6 +578,9 @@ function createCookieMunch(opts) {
|
|
|
283
578
|
}),
|
|
284
579
|
cookiePresets: (cbid) => get(`/sites/${enc(cbid)}/cookies/presets`),
|
|
285
580
|
applyCookiePreset: (cbid, vendor, params, o) => request("PUT", `/sites/${enc(cbid)}/cookies/presets/${enc(vendor)}${qs({ dryRun: o?.dryRun ? "1" : void 0 })}`, params ?? {}),
|
|
581
|
+
importCookieManifest: (cbid, input, o) => request("PUT", `/sites/${enc(cbid)}/cookies/presets/custom${qs({ dryRun: o?.dryRun ? "1" : void 0 })}`, input),
|
|
582
|
+
cookieSuggestions: (cbid) => get(`/sites/${enc(cbid)}/cookies/suggestions`),
|
|
583
|
+
declareCookieSuggestions: (cbid, input, o) => request("POST", `/sites/${enc(cbid)}/cookies/suggestions/declare${qs({ dryRun: o?.dryRun ? "1" : void 0 })}`, input ?? {}),
|
|
286
584
|
removeCookiePreset: (cbid, vendor, o) => request("DELETE", `/sites/${enc(cbid)}/cookies/presets/${enc(vendor)}${qs({ dryRun: o?.dryRun ? "1" : void 0 })}`),
|
|
287
585
|
deleteObservedCookie: (async (cbid, name, o) => {
|
|
288
586
|
const out = await request("DELETE", `/sites/${enc(cbid)}/observed-cookies/${enc(name)}${qs({ dryRun: o?.dryRun ? "1" : void 0 })}`);
|
|
@@ -300,6 +598,8 @@ function createCookieMunch(opts) {
|
|
|
300
598
|
verifyChallenge: (cbid) => get(`/sites/${enc(cbid)}/verify/challenge`),
|
|
301
599
|
createBulk: (sites) => request("POST", "/sites/bulk", { sites }),
|
|
302
600
|
brand: (cbid) => request("POST", `/sites/${enc(cbid)}/brand`, {}),
|
|
601
|
+
getBrand: (cbid) => get(`/sites/${enc(cbid)}/brand`),
|
|
602
|
+
setBrand: (cbid, input, o) => request("PUT", `/sites/${enc(cbid)}/brand${qs({ dryRun: o?.dryRun ? "1" : void 0 })}`, input),
|
|
303
603
|
getFlow: (cbid) => get(`/sites/${enc(cbid)}/flow`),
|
|
304
604
|
editFlow: (cbid, operations) => request("POST", `/sites/${enc(cbid)}/flow/ops`, { operations }),
|
|
305
605
|
setFlow: (cbid, config) => request("PUT", `/sites/${enc(cbid)}/flow`, config)
|
|
@@ -307,11 +607,13 @@ function createCookieMunch(opts) {
|
|
|
307
607
|
consent: {
|
|
308
608
|
verify: (cbid) => get(`/sites/${enc(cbid)}/consent/verify`),
|
|
309
609
|
stats: (cbid, query) => get(`/sites/${enc(cbid)}/consent/stats${qs(toQuery(query))}`),
|
|
610
|
+
insights: (cbid, query) => get(`/sites/${enc(cbid)}/consent/insights${qs(toQuery(query))}`),
|
|
310
611
|
log: (cbid, query) => get(`/sites/${enc(cbid)}/consent/log${qs(toQuery(query))}`),
|
|
311
612
|
export: (cbid, query) => request("GET", `/sites/${enc(cbid)}/consent/export${qs(toQuery(query))}`, void 0, true),
|
|
312
613
|
receipt: (cbid, stamp) => get(`/sites/${enc(cbid)}/receipt/${enc(stamp)}`),
|
|
313
614
|
eraseSubject: (cbid, stamp) => request("POST", `/sites/${enc(cbid)}/erase-consent`, { stamp }),
|
|
314
615
|
exportSubject: (cbid, stamp) => request("GET", `/sites/${enc(cbid)}/subject-export?stamp=${enc(stamp)}`),
|
|
616
|
+
categoryRegistry: () => get("/categories/registry"),
|
|
315
617
|
ingest: async (input) => {
|
|
316
618
|
const res = await doFetch(`${opts.baseUrl.replace(/\/+$/, "")}/api/v1/consent`, {
|
|
317
619
|
method: "POST",
|
|
@@ -342,7 +644,16 @@ function createCookieMunch(opts) {
|
|
|
342
644
|
delete: async (id) => {
|
|
343
645
|
await request("DELETE", `/dsar/${enc(id)}`);
|
|
344
646
|
},
|
|
345
|
-
markTest: (id) => request("POST", `/dsar/${enc(id)}/mark-test`)
|
|
647
|
+
markTest: (id) => request("POST", `/dsar/${enc(id)}/mark-test`),
|
|
648
|
+
listByTrace: (trace) => get(`/dsar${qs({ trace })}`),
|
|
649
|
+
openCase: (input, o) => request("POST", "/dsar/requests", input, false, {
|
|
650
|
+
...o?.idempotencyKey ? { "Idempotency-Key": o.idempotencyKey } : {},
|
|
651
|
+
...o?.trace ? { "X-ForgeDeck-Trace": o.trace } : {}
|
|
652
|
+
}),
|
|
653
|
+
getCase: (id) => get(`/dsar/requests/${enc(id)}`),
|
|
654
|
+
cancelCase: (id) => request("POST", `/dsar/requests/${enc(id)}/cancel`),
|
|
655
|
+
caseReceipt: (id) => get(`/dsar/requests/${enc(id)}/receipt`),
|
|
656
|
+
caseReceiptPdf: (id) => requestBytes(`/dsar/requests/${enc(id)}/receipt?format=pdf`)
|
|
346
657
|
},
|
|
347
658
|
vendors: {
|
|
348
659
|
list: () => get("/vendors"),
|
|
@@ -445,7 +756,8 @@ function createCookieMunch(opts) {
|
|
|
445
756
|
status: (requestId) => get(`/dsar/${enc(requestId)}/fulfillment`),
|
|
446
757
|
pendingTasks: (limit) => get(`/dsar/agent/tasks${qs({ limit })}`),
|
|
447
758
|
reportTask: (taskId, ok, error) => request("POST", `/dsar/agent/tasks/${enc(taskId)}/result`, { ok, ...error !== void 0 ? { error } : {} }),
|
|
448
|
-
executors: () => get(
|
|
759
|
+
executors: (o) => get(`/dsar/executors${metaQs(o)}`),
|
|
760
|
+
executor: (id) => get(`/dsar/executors/${enc(id)}`),
|
|
449
761
|
connectExecutor: (input) => request("POST", "/dsar/executors", input),
|
|
450
762
|
updateExecutor: (id, patch) => request("PATCH", `/dsar/executors/${enc(id)}`, patch),
|
|
451
763
|
dryRunConnectExecutor: (input) => request("POST", "/dsar/executors?dryRun=1", input),
|
|
@@ -460,7 +772,11 @@ function createCookieMunch(opts) {
|
|
|
460
772
|
},
|
|
461
773
|
taskExport: (requestId, taskId) => get(`/dsar/${enc(requestId)}/tasks/${enc(taskId)}/export`),
|
|
462
774
|
task: (requestId, taskId) => get(`/dsar/${enc(requestId)}/tasks/${enc(taskId)}`),
|
|
463
|
-
testExecutor: (id, input) => request("POST", `/dsar/executors/${enc(id)}/test`, input)
|
|
775
|
+
testExecutor: (id, input) => request("POST", `/dsar/executors/${enc(id)}/test`, input),
|
|
776
|
+
recheckExecutor: (id) => request("POST", `/dsar/executors/${enc(id)}/recheck`),
|
|
777
|
+
executorHealthHistory: (id, opts2 = {}) => get(`/dsar/executors/${enc(id)}/health/history${qs({ limit: opts2.limit })}`),
|
|
778
|
+
executorConformance: (id) => request("POST", `/dsar/executors/${enc(id)}/test`, { conformance: true }),
|
|
779
|
+
rollExecutorCompletionSecret: (id) => request("POST", `/dsar/executors/${enc(id)}/completion-secret/roll`)
|
|
464
780
|
},
|
|
465
781
|
preferences: {
|
|
466
782
|
list: () => get("/preferences"),
|
|
@@ -484,11 +800,25 @@ function createCookieMunch(opts) {
|
|
|
484
800
|
},
|
|
485
801
|
roll: (prefix) => request("POST", `/keys/${enc(prefix)}/roll`),
|
|
486
802
|
update: (prefix, patch) => request("PATCH", `/keys/${enc(prefix)}`, patch),
|
|
487
|
-
report: (o) => get(`/keys/report${qs({ unusedDays: o?.unusedDays, expiringDays: o?.expiringDays })}`)
|
|
803
|
+
report: (o) => get(`/keys/report${qs({ unusedDays: o?.unusedDays, expiringDays: o?.expiringDays })}`),
|
|
804
|
+
listStandard: () => get("/keys?shape=standard"),
|
|
805
|
+
self: () => get("/keys/self"),
|
|
806
|
+
rollSelf: () => request("POST", "/keys/self/roll"),
|
|
807
|
+
vend: (input) => request("POST", "/keys/vend", input),
|
|
808
|
+
revokeLeaked: (report) => request("POST", "/keys/revoke_leaked", report)
|
|
488
809
|
},
|
|
489
810
|
usage: () => get("/usage"),
|
|
811
|
+
entitlements: () => get("/entitlements"),
|
|
490
812
|
integrationHealth: (cbid) => get(`/integration-health${qs({ cbid })}`),
|
|
491
|
-
|
|
813
|
+
healthWaivers: {
|
|
814
|
+
list: (cbid) => get(`/integration-health/waivers${qs({ cbid })}`),
|
|
815
|
+
set: (input) => request("PUT", "/integration-health/waivers", input),
|
|
816
|
+
remove: async (check, opts2) => {
|
|
817
|
+
await request("DELETE", `/integration-health/waivers${qs({ check, cbid: opts2?.cbid, subject: opts2?.subject })}`);
|
|
818
|
+
}
|
|
819
|
+
},
|
|
820
|
+
embedManifest: (cbid) => get(`/embed${qs({ cbid })}`),
|
|
821
|
+
provision: (input, o) => request("POST", `/provision${qs({ dryRun: o?.dryRun ? "1" : void 0, fromStep: o?.fromStep })}`, input),
|
|
492
822
|
org: {
|
|
493
823
|
get: () => get("/org"),
|
|
494
824
|
update: (patch) => request("PATCH", "/org", patch),
|
|
@@ -518,7 +848,14 @@ function createCookieMunch(opts) {
|
|
|
518
848
|
remove: () => request("DELETE", "/org/rights-identity")
|
|
519
849
|
}
|
|
520
850
|
},
|
|
521
|
-
audit: (limit) => get(`/audit${qs({ limit })}`),
|
|
851
|
+
audit: (limit, opts2) => get(`/audit${qs({ limit, actor: opts2?.actor })}`),
|
|
852
|
+
auditSink: {
|
|
853
|
+
get: () => get("/audit/sink"),
|
|
854
|
+
set: (input) => request("PUT", "/audit/sink", input),
|
|
855
|
+
remove: async () => {
|
|
856
|
+
await request("DELETE", "/audit/sink");
|
|
857
|
+
}
|
|
858
|
+
},
|
|
522
859
|
assets: {
|
|
523
860
|
upload: (input) => request("POST", "/assets", input),
|
|
524
861
|
delete: async (urlOrFileName) => {
|
|
@@ -541,7 +878,7 @@ function createCookieMunch(opts) {
|
|
|
541
878
|
}
|
|
542
879
|
},
|
|
543
880
|
webhooks: {
|
|
544
|
-
list: () => get(
|
|
881
|
+
list: (o) => get(`/webhooks${metaQs(o)}`),
|
|
545
882
|
create: (input) => request("POST", "/webhooks", input),
|
|
546
883
|
update: (id, patch) => request("PATCH", `/webhooks/${enc(id)}`, patch),
|
|
547
884
|
delete: async (id) => {
|
|
@@ -550,7 +887,8 @@ function createCookieMunch(opts) {
|
|
|
550
887
|
rollSecret: (id) => request("POST", `/webhooks/${enc(id)}/roll`),
|
|
551
888
|
test: (id) => request("POST", `/webhooks/${enc(id)}/test`),
|
|
552
889
|
deadLetters: () => get("/webhooks/dead-letters"),
|
|
553
|
-
replayDeadLetter: (id) => request("POST", `/webhooks/dead-letters/${enc(id)}/replay`)
|
|
890
|
+
replayDeadLetter: (id) => request("POST", `/webhooks/dead-letters/${enc(id)}/replay`),
|
|
891
|
+
replay: (id, since, opts2) => request("POST", `/webhooks/${enc(id)}/replay${qs({ since: String(since), limit: opts2?.limit })}`)
|
|
554
892
|
},
|
|
555
893
|
rightsLinks: {
|
|
556
894
|
create: (cbid, input) => request("POST", `/sites/${enc(cbid)}/rights-links`, input)
|
|
@@ -570,6 +908,13 @@ function createCookieMunch(opts) {
|
|
|
570
908
|
set: (cbid, input) => request("PUT", `/sites/${enc(cbid)}/rights-identity`, input),
|
|
571
909
|
remove: (cbid) => request("DELETE", `/sites/${enc(cbid)}/rights-identity`)
|
|
572
910
|
},
|
|
911
|
+
dsarSettings: {
|
|
912
|
+
get: (cbid) => get(`/sites/${enc(cbid)}/dsar-settings`),
|
|
913
|
+
set: (cbid, input) => request("PUT", `/sites/${enc(cbid)}/dsar-settings`, input)
|
|
914
|
+
},
|
|
915
|
+
integrations: {
|
|
916
|
+
subjectCard: (app, person) => get(`/integrations/${enc(app)}/card${qs(person)}`)
|
|
917
|
+
},
|
|
573
918
|
subjects: {
|
|
574
919
|
consent: (subjectId) => get(`/subjects/${enc(subjectId)}/consent`)
|
|
575
920
|
},
|
|
@@ -589,15 +934,23 @@ function createCookieMunch(opts) {
|
|
|
589
934
|
}
|
|
590
935
|
// Annotate the CommonJS export names for ESM import in node:
|
|
591
936
|
0 && (module.exports = {
|
|
937
|
+
CANARY_ISSUER,
|
|
938
|
+
CONFORMANCE_RULES,
|
|
592
939
|
CONSENT_COOKIE_NAME,
|
|
593
940
|
CookieMunchApiError,
|
|
941
|
+
DEFAULT_JWKS_URL,
|
|
594
942
|
SDK_VERSION,
|
|
943
|
+
canaryIdentityFor,
|
|
944
|
+
consentSignaturePayload,
|
|
595
945
|
createCookieMunch,
|
|
596
946
|
getConsent,
|
|
597
947
|
isAllowed,
|
|
598
948
|
mintConsentSyncToken,
|
|
599
949
|
parseConsentValue,
|
|
600
950
|
readConsent,
|
|
951
|
+
runExecutorConformance,
|
|
952
|
+
verifyDecisionSignature,
|
|
953
|
+
verifyToken,
|
|
601
954
|
verifyWebhookSignature
|
|
602
955
|
});
|
|
603
956
|
//# sourceMappingURL=index.cjs.map
|