@cookiemunch/sdk 0.9.1 → 0.11.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 CHANGED
@@ -2,6 +2,22 @@
2
2
 
3
3
  Every published release, newest first. Dates are the day the version was cut.
4
4
 
5
+ ## 0.11.0 (2026-11-02)
6
+
7
+ - Added: `anchors.create` / `list` / `get`: signed, timestamped receipts for the head of your own hash-chained log (`POST /v1/anchors`).
8
+ - Added: `health.version` (the system's last `X-Version`) on executor connections; `skipped` on a created request.
9
+ - Changed: `cookies.declare` with `replace: true` leaves vendor-preset rows alone.
10
+
11
+ ## 0.10.0 (2026-11-01)
12
+
13
+ - Added: `fulfillment.approveTask` / `rejectTask` for an erasure parked by `requireApproval`; `requireApproval` on connections; `verify: 1 | 'warn'` on `connectExecutor`; `listByOrigin` (`origin: "account_deletion"`).
14
+ - Added: `keepOldDomain` on `sites.update`.
15
+
16
+ - Added: `createRightsHandlers()` from `@cookiemunch/sdk/rights`: the cm-standard-v2 executor contract as Fetch API handlers built from your callbacks. It covers the key check, signed request ids, `executeAfter`, cancel, idempotency and the canary.
17
+ - Changed: the conformance kit's `auth.required` also passes a bare `404` (no `X-Version`) for a request without the credential.
18
+ - Added: `secret` on `webhooks.create` and `webhooks.rollSecret(id, { secret })`: your own signing secret, stored and not echoed back.
19
+ - Added: webhook `events` take `*` and `<family>.*`; a subscription's `matches` lists the events its patterns take today.
20
+
5
21
  ## 0.9.1 (2026-10-30)
6
22
 
7
23
  - Added: cookie preset params `publishableKey` (Atlas: read the instance's own list by its public `GET /v1/cookie_manifest` when the site's Atlas key cannot), and Chatly's `storage` and `tracking` (the widget's own options, which decide whether `chatly:seen` and `chatly:utm` are declared).
package/README.md CHANGED
@@ -65,3 +65,21 @@ const ok = verifyWebhookSignature({
65
65
  ```
66
66
 
67
67
  Full recipe (Express, edge/WebCrypto, replay protection): [`docs/api/webhook-signing.md`](../../docs/api/webhook-signing.md).
68
+
69
+ ## Answering rights requests
70
+
71
+ `createRightsHandlers(...)` from `@cookiemunch/sdk/rights` serves the
72
+ [cm-standard-v2 executor contract](https://cookiemunch.net/docs/executor-contract-v2) as Fetch API
73
+ handlers built from your callbacks. Cookie Munch then finds, exports and erases people in your product.
74
+
75
+ ```ts
76
+ import { createRightsHandlers } from '@cookiemunch/sdk/rights';
77
+
78
+ export const { GET, POST } = createRightsHandlers({
79
+ key: process.env.CM_EXECUTOR_KEY!, // the secretKey you registered
80
+ idSecret: process.env.CM_REQUEST_ID_SECRET!, // signs request ids; 32+ characters
81
+ lookup: ({ emails, identity }) => findUserId(emails, identity),
82
+ access: async (r) => ({ status: 'completed', result: await exportUser(r.subjectId) }),
83
+ erase: async (r) => { await deleteUser(r.subjectId); },
84
+ });
85
+ ```
@@ -0,0 +1,182 @@
1
+ // src/executor-conformance.ts
2
+ var CANARY_ISSUER = "https://canary.cookiemunch.net";
3
+ var CANARY_EMAIL_RE = /^canary-([0-9a-f]{16})@canary\.cookiemunch\.net$/;
4
+ function canaryIdentityFor(canaryEmail) {
5
+ const m = CANARY_EMAIL_RE.exec(canaryEmail.trim().toLowerCase());
6
+ if (!m) throw new Error("canaryEmail must be the canary address canary-<16 hex>@canary.cookiemunch.net");
7
+ return { iss: CANARY_ISSUER, sub: `canary_${m[1]}`, ext: `canary:${m[1]}` };
8
+ }
9
+ var CONFORMANCE_RULES = [
10
+ { id: "auth.required", title: "A request without the credential is refused (401, 403, or a 404 without X-Version)." },
11
+ { id: "lookup.email.found", title: "The canary subject is found by its email." },
12
+ { id: "lookup.identity.found", title: "The canary subject is found by its identity alone (iss + sub), and it is the same subject." },
13
+ { id: "lookup.unknown.no_match", title: 'An address nobody has answers { subjectId: null, status: "no_match" }.' },
14
+ { id: "lookup.identity_beats_email", title: "An identity that matches no one is never matched through the email of someone else." },
15
+ { id: "requests.bad_body.400", title: "A request with an unknown type answers 400 { error, field }." },
16
+ { id: "requests.alias.canonical_type", title: 'An alias (erasure) is accepted and answered with canonicalType "deletion".' },
17
+ { 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.' },
18
+ { id: "requests.idempotency.replay", title: "The same Idempotency-Key with the same body returns the same request, not a second one." },
19
+ { id: "requests.idempotency.mismatch_409", title: "The same Idempotency-Key with a different body answers 409." },
20
+ { id: "requests.get", title: "GET /privacy/requests/{id} returns the request, still scheduled." },
21
+ { id: "requests.unknown.404", title: "An unknown request id answers 404." },
22
+ { id: "requests.cancel.before_execute_after", title: 'A cancel before executeAfter answers status "cancelled".' },
23
+ { id: "requests.cancel.keeps_data", title: "After a cancel the subject is still there: a lookup finds it." },
24
+ { id: "version.header", title: "Answers carry X-Version (the highest contract version the executor supports)." }
25
+ ];
26
+ function randomTag() {
27
+ const bytes = new Uint8Array(8);
28
+ const c = globalThis.crypto;
29
+ if (c?.getRandomValues) c.getRandomValues(bytes);
30
+ else for (let i = 0; i < bytes.length; i++) bytes[i] = Math.floor(Math.random() * 256);
31
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
32
+ }
33
+ function field(json, key) {
34
+ return json && typeof json === "object" ? json[key] : void 0;
35
+ }
36
+ async function runExecutorConformance(o) {
37
+ const doFetch = o.fetch ?? fetch;
38
+ const now = o.now ?? (() => Date.now());
39
+ const base = o.baseUrl.replace(/\/+$/, "") + (o.prefix ?? "/privacy");
40
+ let url;
41
+ try {
42
+ url = new URL(base);
43
+ } catch {
44
+ throw new Error("baseUrl must be an absolute URL");
45
+ }
46
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && /^(localhost|127\.0\.0\.1|\[::1\])$/.test(url.hostname))) {
47
+ throw new Error("baseUrl must be https (http only for localhost)");
48
+ }
49
+ const identity = canaryIdentityFor(o.canaryEmail);
50
+ const canaryEmail = o.canaryEmail.trim().toLowerCase();
51
+ const auth = o.auth ?? { header: "Authorization", format: "Bearer {secret}" };
52
+ const run = o.runId ?? randomTag();
53
+ const results = /* @__PURE__ */ new Map();
54
+ let version;
55
+ const set = (id, status, detail) => {
56
+ const rule = CONFORMANCE_RULES.find((r) => r.id === id);
57
+ results.set(id, { id, title: rule.title, status, ...detail ? { detail } : {} });
58
+ };
59
+ const pass = (id) => set(id, "pass");
60
+ const fail = (id, detail) => set(id, "fail", detail);
61
+ const skip = (id, detail) => set(id, "skip", detail);
62
+ async function call(method, path, body, extra = {}, withAuth = true) {
63
+ const headers = {
64
+ accept: "application/json",
65
+ "X-Version": "2",
66
+ ...withAuth ? { [auth.header]: auth.format.replace("{secret}", o.secret) } : {},
67
+ ...body !== void 0 ? { "content-type": "application/json" } : {},
68
+ ...extra
69
+ };
70
+ try {
71
+ const res = await doFetch(base + path, { method, headers, ...body !== void 0 ? { body: JSON.stringify(body) } : {} });
72
+ const text = await res.text().catch(() => "");
73
+ let json;
74
+ try {
75
+ json = text ? JSON.parse(text) : void 0;
76
+ } catch {
77
+ json = void 0;
78
+ }
79
+ const v = res.headers.get("x-version");
80
+ if (v && !version) version = v;
81
+ return { status: res.status, json, version: v };
82
+ } catch (err) {
83
+ return { status: 0, json: void 0, version: null, error: err instanceof Error ? err.message : String(err) };
84
+ }
85
+ }
86
+ const said = (a) => a.status === 0 ? `unreachable: ${a.error}` : `HTTP ${a.status} ${JSON.stringify(a.json ?? null).slice(0, 200)}`;
87
+ const key = (rule) => `conformance:${run}:${rule}`;
88
+ const anon = await call("POST", "/lookup", { emails: [canaryEmail] }, {}, false);
89
+ if (anon.status === 0) {
90
+ for (const r of CONFORMANCE_RULES) skip(r.id, said(anon));
91
+ fail("auth.required", said(anon));
92
+ return report();
93
+ }
94
+ if (anon.status === 401 || anon.status === 403 || anon.status === 404 && !anon.version) pass("auth.required");
95
+ else fail("auth.required", `without a credential: ${said(anon)}`);
96
+ const byEmail = await call("POST", "/lookup", { emails: [canaryEmail] }, { "Idempotency-Key": key("lookup-email") });
97
+ const subjectId = field(byEmail.json, "subjectId");
98
+ if (byEmail.status === 200 && field(byEmail.json, "status") === "found" && typeof subjectId === "string" && subjectId) pass("lookup.email.found");
99
+ else fail("lookup.email.found", `the canary address: ${said(byEmail)}`);
100
+ const byIdentity = await call("POST", "/lookup", { emails: [], identity, atlas: identity }, { "Idempotency-Key": key("lookup-identity") });
101
+ if (byIdentity.status === 200 && field(byIdentity.json, "status") === "found" && field(byIdentity.json, "subjectId") === subjectId && typeof subjectId === "string") {
102
+ pass("lookup.identity.found");
103
+ } else {
104
+ fail("lookup.identity.found", `the canary identity ${identity.sub}: ${said(byIdentity)}${typeof subjectId === "string" ? ` (by email it was ${subjectId})` : ""}`);
105
+ }
106
+ const nobody = await call("POST", "/lookup", { emails: [`nobody-${run}@canary.cookiemunch.net`] }, { "Idempotency-Key": key("lookup-nobody") });
107
+ if (nobody.status === 200 && field(nobody.json, "status") === "no_match" && field(nobody.json, "subjectId") === null) pass("lookup.unknown.no_match");
108
+ else fail("lookup.unknown.no_match", said(nobody));
109
+ const stranger = { iss: identity.iss, sub: `canary_other_${run}` };
110
+ const crossed = await call("POST", "/lookup", { emails: [canaryEmail], identity: stranger, atlas: stranger }, { "Idempotency-Key": key("lookup-crossed") });
111
+ if (crossed.status === 200 && field(crossed.json, "status") === "no_match") pass("lookup.identity_beats_email");
112
+ else {
113
+ fail(
114
+ "lookup.identity_beats_email",
115
+ `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`
116
+ );
117
+ }
118
+ const bad = await call("POST", "/requests", { subjectId: subjectId ?? "x", type: "teleport", caseId: `conformance-${run}` }, { "Idempotency-Key": key("bad-body") });
119
+ if (bad.status === 400 && typeof field(bad.json, "error") === "string" && typeof field(bad.json, "field") === "string") pass("requests.bad_body.400");
120
+ else fail("requests.bad_body.400", said(bad));
121
+ if (typeof subjectId !== "string") {
122
+ 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"]) {
123
+ skip(id, "needs the canary subject, which the lookup did not find");
124
+ }
125
+ } else {
126
+ const executeAfter = new Date(Math.ceil((now() + 36e5) / 1e3) * 1e3).toISOString();
127
+ const body = { subjectId, type: "erasure", executeAfter, caseId: `conformance-${run}`, reason: "Cookie Munch conformance check (canary subject)" };
128
+ const opened = await call("POST", "/requests", body, { "Idempotency-Key": key("open") });
129
+ const id = field(opened.json, "id");
130
+ const st = field(opened.json, "status");
131
+ if ((opened.status === 200 || opened.status === 201) && field(opened.json, "canonicalType") === "deletion") pass("requests.alias.canonical_type");
132
+ else fail("requests.alias.canonical_type", `type "erasure": ${said(opened)}`);
133
+ const eta = field(opened.json, "eta");
134
+ if ((opened.status === 200 || opened.status === 201) && st === "scheduled" && typeof eta === "string" && Date.parse(eta) === Date.parse(executeAfter)) {
135
+ pass("requests.execute_after.scheduled");
136
+ } else if (st === "completed" || st === "completed_with_retention" || st === "processing") {
137
+ fail("requests.execute_after.scheduled", `executeAfter is ${executeAfter}, an hour away, and the executor answered "${String(st)}": it acted before executeAfter`);
138
+ } else {
139
+ fail("requests.execute_after.scheduled", `expected status "scheduled" with eta ${executeAfter}: ${said(opened)}`);
140
+ }
141
+ const replay = await call("POST", "/requests", body, { "Idempotency-Key": key("open") });
142
+ if (typeof id === "string" && (replay.status === 200 || replay.status === 201) && field(replay.json, "id") === id) pass("requests.idempotency.replay");
143
+ else fail("requests.idempotency.replay", `a retry with the same key: ${said(replay)}${typeof id === "string" ? ` (first answer id ${id})` : ""}`);
144
+ const mismatch = await call("POST", "/requests", { ...body, reason: "a different body" }, { "Idempotency-Key": key("open") });
145
+ if (mismatch.status === 409) pass("requests.idempotency.mismatch_409");
146
+ else fail("requests.idempotency.mismatch_409", `the same key with a different body: ${said(mismatch)}`);
147
+ if (typeof id !== "string") {
148
+ skip("requests.get", "no request id came back");
149
+ skip("requests.cancel.before_execute_after", "no request id came back");
150
+ skip("requests.cancel.keeps_data", "no request id came back");
151
+ } else {
152
+ const got = await call("GET", `/requests/${encodeURIComponent(id)}`, void 0, { "Idempotency-Key": key("get") });
153
+ if (got.status === 200 && field(got.json, "id") === id && field(got.json, "status") === "scheduled") pass("requests.get");
154
+ else fail("requests.get", said(got));
155
+ const cancelled = await call("POST", `/requests/${encodeURIComponent(id)}/cancel`, {}, { "Idempotency-Key": key("cancel") });
156
+ if (cancelled.status === 200 && field(cancelled.json, "status") === "cancelled") pass("requests.cancel.before_execute_after");
157
+ else fail("requests.cancel.before_execute_after", said(cancelled));
158
+ const after = await call("POST", "/lookup", { emails: [canaryEmail] }, { "Idempotency-Key": key("lookup-after-cancel") });
159
+ if (after.status === 200 && field(after.json, "status") === "found") pass("requests.cancel.keeps_data");
160
+ else fail("requests.cancel.keeps_data", `a lookup after the cancel: ${said(after)} \u2014 a cancel before executeAfter must leave the data intact`);
161
+ }
162
+ }
163
+ const unknown = await call("GET", `/requests/conformance-missing-${run}`, void 0, { "Idempotency-Key": key("unknown") });
164
+ if (unknown.status === 404) pass("requests.unknown.404");
165
+ else fail("requests.unknown.404", said(unknown));
166
+ if (version) pass("version.header");
167
+ else fail("version.header", "no answer carried an X-Version header");
168
+ return report();
169
+ function report() {
170
+ const rules = CONFORMANCE_RULES.map((r) => results.get(r.id) ?? { id: r.id, title: r.title, status: "skip", detail: "not reached" });
171
+ const count = (s) => rules.filter((r) => r.status === s).length;
172
+ return { ok: count("fail") === 0, passed: count("pass"), failed: count("fail"), skipped: count("skip"), rules, ...version ? { version } : {} };
173
+ }
174
+ }
175
+
176
+ export {
177
+ CANARY_ISSUER,
178
+ canaryIdentityFor,
179
+ CONFORMANCE_RULES,
180
+ runExecutorConformance
181
+ };
182
+ //# sourceMappingURL=chunk-U5OJSPE5.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/executor-conformance.ts"],"sourcesContent":["/**\n * The cm-standard-v2 executor conformance kit (CMU-X20).\n *\n * cm-standard-v2 is an open contract: any system that holds personal data can implement it and\n * be driven by Cookie Munch with no code on our side (docs: cookiemunch.net/docs/executor-contract-v2).\n * This kit checks an implementation rule by rule, and names the rule that fails. It is the same\n * suite Cookie Munch runs on `POST /v1/dsar/executors/{id}/test {conformance: true}`, and the\n * `cookiemunch executors conformance <baseUrl>` command runs it from a terminal or CI.\n *\n * It only ever touches the CANARY subject — an address no real person has. The contract asks every\n * implementation to treat the canary as an existing synthetic person: it is found by its email and\n * by its identity, requests on it move through the full state machine, and nothing real is ever\n * erased. So the kit can check that an erasure waits for `executeAfter`, that a cancel leaves the\n * person in place, and that an identity is never matched to someone else through a shared email —\n * without a real person's data being anywhere near it.\n *\n * No dependencies: `fetch` only, so it runs in Node 18+, a browser, or an edge runtime.\n */\n\nexport type ConformanceStatus = 'pass' | 'fail' | 'skip';\n\nexport interface ConformanceRuleResult {\n /** A stable id, e.g. `requests.execute_after.scheduled`. */\n id: string;\n /** What the rule says, in one sentence. */\n title: string;\n status: ConformanceStatus;\n /** What was seen, when it failed or was skipped. */\n detail?: string;\n}\n\nexport interface ConformanceReport {\n /** True when no rule failed. */\n ok: boolean;\n passed: number;\n failed: number;\n skipped: number;\n rules: ConformanceRuleResult[];\n /** The `X-Version` the executor answered with, when it sent one. */\n version?: string;\n}\n\nexport interface ConformanceOptions {\n /** The executor's base: the contract paths are `{baseUrl}/privacy/lookup` and so on. https (http only for localhost). */\n baseUrl: string;\n /** The credential Cookie Munch would send. */\n secret: string;\n /**\n * The canary address: `canary-<16 hex>@canary.cookiemunch.net`, per organisation\n * (`GET /v1/dsar/executors/presets` → `canaryEmail`).\n */\n canaryEmail: string;\n /** How the secret is sent. Default `Authorization: Bearer {secret}`. */\n auth?: { header: string; format: string };\n /** Path prefix under baseUrl. Default `/privacy`. */\n prefix?: string;\n fetch?: typeof fetch;\n now?: () => number;\n /** A per-run tag for idempotency keys and case ids. Default random. */\n runId?: string;\n}\n\n/** The issuer the canary identity carries. */\nexport const CANARY_ISSUER = 'https://canary.cookiemunch.net';\nconst CANARY_EMAIL_RE = /^canary-([0-9a-f]{16})@canary\\.cookiemunch\\.net$/;\n\n/** The canary subject's identity, derived from its address. */\nexport function canaryIdentityFor(canaryEmail: string): { iss: string; sub: string; ext: string } {\n const m = CANARY_EMAIL_RE.exec(canaryEmail.trim().toLowerCase());\n if (!m) throw new Error('canaryEmail must be the canary address canary-<16 hex>@canary.cookiemunch.net');\n return { iss: CANARY_ISSUER, sub: `canary_${m[1]}`, ext: `canary:${m[1]}` };\n}\n\n/** Every rule, in the order they run. */\nexport const CONFORMANCE_RULES: ReadonlyArray<{ id: string; title: string }> = [\n { id: 'auth.required', title: 'A request without the credential is refused (401, 403, or a 404 without X-Version).' },\n { id: 'lookup.email.found', title: 'The canary subject is found by its email.' },\n { id: 'lookup.identity.found', title: 'The canary subject is found by its identity alone (iss + sub), and it is the same subject.' },\n { id: 'lookup.unknown.no_match', title: 'An address nobody has answers { subjectId: null, status: \"no_match\" }.' },\n { id: 'lookup.identity_beats_email', title: 'An identity that matches no one is never matched through the email of someone else.' },\n { id: 'requests.bad_body.400', title: 'A request with an unknown type answers 400 { error, field }.' },\n { id: 'requests.alias.canonical_type', title: 'An alias (erasure) is accepted and answered with canonicalType \"deletion\".' },\n { id: 'requests.execute_after.scheduled', title: 'A deletion with a future executeAfter is \"scheduled\" with eta equal to executeAfter — nothing is deleted before it.' },\n { id: 'requests.idempotency.replay', title: 'The same Idempotency-Key with the same body returns the same request, not a second one.' },\n { id: 'requests.idempotency.mismatch_409', title: 'The same Idempotency-Key with a different body answers 409.' },\n { id: 'requests.get', title: 'GET /privacy/requests/{id} returns the request, still scheduled.' },\n { id: 'requests.unknown.404', title: 'An unknown request id answers 404.' },\n { id: 'requests.cancel.before_execute_after', title: 'A cancel before executeAfter answers status \"cancelled\".' },\n { id: 'requests.cancel.keeps_data', title: 'After a cancel the subject is still there: a lookup finds it.' },\n { id: 'version.header', title: 'Answers carry X-Version (the highest contract version the executor supports).' },\n];\n\ntype Answer = { status: number; json: unknown; version: string | null; error?: string };\n\nfunction randomTag(): string {\n const bytes = new Uint8Array(8);\n const c = (globalThis as { crypto?: { getRandomValues?: (a: Uint8Array) => Uint8Array } }).crypto;\n if (c?.getRandomValues) c.getRandomValues(bytes);\n else for (let i = 0; i < bytes.length; i++) bytes[i] = Math.floor(Math.random() * 256);\n return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');\n}\n\nfunction field(json: unknown, key: string): unknown {\n return json && typeof json === 'object' ? (json as Record<string, unknown>)[key] : undefined;\n}\n\n/** Run every rule against one executor. Never throws for an executor's answer; only for bad options. */\nexport async function runExecutorConformance(o: ConformanceOptions): Promise<ConformanceReport> {\n const doFetch = o.fetch ?? fetch;\n const now = o.now ?? (() => Date.now());\n const base = o.baseUrl.replace(/\\/+$/, '') + (o.prefix ?? '/privacy');\n let url: URL;\n try {\n url = new URL(base);\n } catch {\n throw new Error('baseUrl must be an absolute URL');\n }\n if (url.protocol !== 'https:' && !(url.protocol === 'http:' && /^(localhost|127\\.0\\.0\\.1|\\[::1\\])$/.test(url.hostname))) {\n throw new Error('baseUrl must be https (http only for localhost)');\n }\n const identity = canaryIdentityFor(o.canaryEmail);\n const canaryEmail = o.canaryEmail.trim().toLowerCase();\n const auth = o.auth ?? { header: 'Authorization', format: 'Bearer {secret}' };\n const run = o.runId ?? randomTag();\n const results = new Map<string, ConformanceRuleResult>();\n let version: string | undefined;\n\n const set = (id: string, status: ConformanceStatus, detail?: string): void => {\n const rule = CONFORMANCE_RULES.find((r) => r.id === id)!;\n results.set(id, { id, title: rule.title, status, ...(detail ? { detail } : {}) });\n };\n const pass = (id: string) => set(id, 'pass');\n const fail = (id: string, detail: string) => set(id, 'fail', detail);\n const skip = (id: string, detail: string) => set(id, 'skip', detail);\n\n async function call(method: string, path: string, body?: unknown, extra: Record<string, string> = {}, withAuth = true): Promise<Answer> {\n const headers: Record<string, string> = {\n accept: 'application/json',\n 'X-Version': '2',\n ...(withAuth ? { [auth.header]: auth.format.replace('{secret}', o.secret) } : {}),\n ...(body !== undefined ? { 'content-type': 'application/json' } : {}),\n ...extra,\n };\n try {\n const res = await doFetch(base + path, { method, headers, ...(body !== undefined ? { body: JSON.stringify(body) } : {}) });\n const text = await res.text().catch(() => '');\n let json: unknown;\n try {\n json = text ? JSON.parse(text) : undefined;\n } catch {\n json = undefined;\n }\n const v = res.headers.get('x-version');\n if (v && !version) version = v;\n return { status: res.status, json, version: v };\n } catch (err) {\n return { status: 0, json: undefined, version: null, error: err instanceof Error ? err.message : String(err) };\n }\n }\n const said = (a: Answer): string => (a.status === 0 ? `unreachable: ${a.error}` : `HTTP ${a.status} ${JSON.stringify(a.json ?? null).slice(0, 200)}`);\n const key = (rule: string) => `conformance:${run}:${rule}`;\n\n // auth.required\n const anon = await call('POST', '/lookup', { emails: [canaryEmail] }, {}, false);\n if (anon.status === 0) {\n // Nothing else can be judged against a system we cannot reach.\n for (const r of CONFORMANCE_RULES) skip(r.id, said(anon));\n fail('auth.required', said(anon));\n return report();\n }\n // A bare 404 (no X-Version) is a refusal that does not even admit the endpoint exists.\n if (anon.status === 401 || anon.status === 403 || (anon.status === 404 && !anon.version)) pass('auth.required');\n else fail('auth.required', `without a credential: ${said(anon)}`);\n\n // lookups\n const byEmail = await call('POST', '/lookup', { emails: [canaryEmail] }, { 'Idempotency-Key': key('lookup-email') });\n const subjectId = field(byEmail.json, 'subjectId');\n if (byEmail.status === 200 && field(byEmail.json, 'status') === 'found' && typeof subjectId === 'string' && subjectId) pass('lookup.email.found');\n else fail('lookup.email.found', `the canary address: ${said(byEmail)}`);\n\n const byIdentity = await call('POST', '/lookup', { emails: [], identity: identity, atlas: identity }, { 'Idempotency-Key': key('lookup-identity') });\n if (byIdentity.status === 200 && field(byIdentity.json, 'status') === 'found' && field(byIdentity.json, 'subjectId') === subjectId && typeof subjectId === 'string') {\n pass('lookup.identity.found');\n } else {\n fail('lookup.identity.found', `the canary identity ${identity.sub}: ${said(byIdentity)}${typeof subjectId === 'string' ? ` (by email it was ${subjectId})` : ''}`);\n }\n\n const nobody = await call('POST', '/lookup', { emails: [`nobody-${run}@canary.cookiemunch.net`] }, { 'Idempotency-Key': key('lookup-nobody') });\n if (nobody.status === 200 && field(nobody.json, 'status') === 'no_match' && field(nobody.json, 'subjectId') === null) pass('lookup.unknown.no_match');\n else fail('lookup.unknown.no_match', said(nobody));\n\n const stranger = { iss: identity.iss, sub: `canary_other_${run}` };\n const crossed = await call('POST', '/lookup', { emails: [canaryEmail], identity: stranger, atlas: stranger }, { 'Idempotency-Key': key('lookup-crossed') });\n if (crossed.status === 200 && field(crossed.json, 'status') === 'no_match') pass('lookup.identity_beats_email');\n else {\n fail(\n 'lookup.identity_beats_email',\n `an identity nobody has (${stranger.sub}) with the canary's email: ${said(crossed)} — the email belongs to a different identity, so this must be no_match`,\n );\n }\n\n // requests\n const bad = await call('POST', '/requests', { subjectId: subjectId ?? 'x', type: 'teleport', caseId: `conformance-${run}` }, { 'Idempotency-Key': key('bad-body') });\n if (bad.status === 400 && typeof field(bad.json, 'error') === 'string' && typeof field(bad.json, 'field') === 'string') pass('requests.bad_body.400');\n else fail('requests.bad_body.400', said(bad));\n\n if (typeof subjectId !== 'string') {\n 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']) {\n skip(id, 'needs the canary subject, which the lookup did not find');\n }\n } else {\n const executeAfter = new Date(Math.ceil((now() + 3_600_000) / 1000) * 1000).toISOString();\n const body = { subjectId, type: 'erasure', executeAfter, caseId: `conformance-${run}`, reason: 'Cookie Munch conformance check (canary subject)' };\n const opened = await call('POST', '/requests', body, { 'Idempotency-Key': key('open') });\n const id = field(opened.json, 'id');\n const st = field(opened.json, 'status');\n if ((opened.status === 200 || opened.status === 201) && field(opened.json, 'canonicalType') === 'deletion') pass('requests.alias.canonical_type');\n else fail('requests.alias.canonical_type', `type \"erasure\": ${said(opened)}`);\n\n const eta = field(opened.json, 'eta');\n if ((opened.status === 200 || opened.status === 201) && st === 'scheduled' && typeof eta === 'string' && Date.parse(eta) === Date.parse(executeAfter)) {\n pass('requests.execute_after.scheduled');\n } else if (st === 'completed' || st === 'completed_with_retention' || st === 'processing') {\n fail('requests.execute_after.scheduled', `executeAfter is ${executeAfter}, an hour away, and the executor answered \"${String(st)}\": it acted before executeAfter`);\n } else {\n fail('requests.execute_after.scheduled', `expected status \"scheduled\" with eta ${executeAfter}: ${said(opened)}`);\n }\n\n const replay = await call('POST', '/requests', body, { 'Idempotency-Key': key('open') });\n if (typeof id === 'string' && (replay.status === 200 || replay.status === 201) && field(replay.json, 'id') === id) pass('requests.idempotency.replay');\n else fail('requests.idempotency.replay', `a retry with the same key: ${said(replay)}${typeof id === 'string' ? ` (first answer id ${id})` : ''}`);\n\n const mismatch = await call('POST', '/requests', { ...body, reason: 'a different body' }, { 'Idempotency-Key': key('open') });\n if (mismatch.status === 409) pass('requests.idempotency.mismatch_409');\n else fail('requests.idempotency.mismatch_409', `the same key with a different body: ${said(mismatch)}`);\n\n if (typeof id !== 'string') {\n skip('requests.get', 'no request id came back');\n skip('requests.cancel.before_execute_after', 'no request id came back');\n skip('requests.cancel.keeps_data', 'no request id came back');\n } else {\n const got = await call('GET', `/requests/${encodeURIComponent(id)}`, undefined, { 'Idempotency-Key': key('get') });\n if (got.status === 200 && field(got.json, 'id') === id && field(got.json, 'status') === 'scheduled') pass('requests.get');\n else fail('requests.get', said(got));\n\n const cancelled = await call('POST', `/requests/${encodeURIComponent(id)}/cancel`, {}, { 'Idempotency-Key': key('cancel') });\n if (cancelled.status === 200 && field(cancelled.json, 'status') === 'cancelled') pass('requests.cancel.before_execute_after');\n else fail('requests.cancel.before_execute_after', said(cancelled));\n\n const after = await call('POST', '/lookup', { emails: [canaryEmail] }, { 'Idempotency-Key': key('lookup-after-cancel') });\n if (after.status === 200 && field(after.json, 'status') === 'found') pass('requests.cancel.keeps_data');\n else fail('requests.cancel.keeps_data', `a lookup after the cancel: ${said(after)} — a cancel before executeAfter must leave the data intact`);\n }\n }\n\n const unknown = await call('GET', `/requests/conformance-missing-${run}`, undefined, { 'Idempotency-Key': key('unknown') });\n if (unknown.status === 404) pass('requests.unknown.404');\n else fail('requests.unknown.404', said(unknown));\n\n if (version) pass('version.header');\n else fail('version.header', 'no answer carried an X-Version header');\n\n return report();\n\n function report(): ConformanceReport {\n const rules = CONFORMANCE_RULES.map((r) => results.get(r.id) ?? { id: r.id, title: r.title, status: 'skip' as const, detail: 'not reached' });\n const count = (s: ConformanceStatus) => rules.filter((r) => r.status === s).length;\n return { ok: count('fail') === 0, passed: count('pass'), failed: count('fail'), skipped: count('skip'), rules, ...(version ? { version } : {}) };\n }\n}\n"],"mappings":"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","names":[]}
package/dist/index.cjs CHANGED
@@ -262,7 +262,7 @@ function canaryIdentityFor(canaryEmail) {
262
262
  return { iss: CANARY_ISSUER, sub: `canary_${m[1]}`, ext: `canary:${m[1]}` };
263
263
  }
264
264
  var CONFORMANCE_RULES = [
265
- { id: "auth.required", title: "A request without the credential is refused (401 or 403)." },
265
+ { id: "auth.required", title: "A request without the credential is refused (401, 403, or a 404 without X-Version)." },
266
266
  { id: "lookup.email.found", title: "The canary subject is found by its email." },
267
267
  { id: "lookup.identity.found", title: "The canary subject is found by its identity alone (iss + sub), and it is the same subject." },
268
268
  { id: "lookup.unknown.no_match", title: 'An address nobody has answers { subjectId: null, status: "no_match" }.' },
@@ -346,7 +346,7 @@ async function runExecutorConformance(o) {
346
346
  fail("auth.required", said(anon));
347
347
  return report();
348
348
  }
349
- if (anon.status === 401 || anon.status === 403) pass("auth.required");
349
+ if (anon.status === 401 || anon.status === 403 || anon.status === 404 && !anon.version) pass("auth.required");
350
350
  else fail("auth.required", `without a credential: ${said(anon)}`);
351
351
  const byEmail = await call("POST", "/lookup", { emails: [canaryEmail] }, { "Idempotency-Key": key("lookup-email") });
352
352
  const subjectId = field(byEmail.json, "subjectId");
@@ -465,7 +465,7 @@ async function exchangeOperatorToken(input) {
465
465
  }
466
466
 
467
467
  // src/index.ts
468
- var SDK_VERSION = "0.9.1";
468
+ var SDK_VERSION = "0.11.0";
469
469
  var CookieMunchApiError = class extends Error {
470
470
  constructor(status, message, code, body) {
471
471
  super(message);
@@ -685,6 +685,7 @@ function createCookieMunch(opts) {
685
685
  },
686
686
  markTest: (id) => request("POST", `/dsar/${enc(id)}/mark-test`),
687
687
  listByTrace: (trace) => get(`/dsar${qs({ trace })}`),
688
+ listByOrigin: (origin) => get(`/dsar${qs({ origin })}`),
688
689
  openCase: (input, o) => request("POST", "/dsar/requests", input, false, {
689
690
  ...o?.idempotencyKey ? { "Idempotency-Key": o.idempotencyKey } : {},
690
691
  ...o?.trace ? { "X-Trace-Id": o.trace } : {}
@@ -798,7 +799,7 @@ function createCookieMunch(opts) {
798
799
  reportTask: (taskId, ok, error) => request("POST", `/dsar/agent/tasks/${enc(taskId)}/result`, { ok, ...error !== void 0 ? { error } : {} }),
799
800
  executors: (o) => get(`/dsar/executors${metaQs(o)}`),
800
801
  executor: (id) => get(`/dsar/executors/${enc(id)}`),
801
- connectExecutor: (input) => request("POST", "/dsar/executors", input),
802
+ connectExecutor: (input, o) => request("POST", `/dsar/executors${qs({ verify: o?.verify ? o.verify === "warn" ? "warn" : "1" : void 0 })}`, input),
802
803
  updateExecutor: (id, patch) => request("PATCH", `/dsar/executors/${enc(id)}`, patch),
803
804
  dryRunConnectExecutor: (input) => request("POST", "/dsar/executors?dryRun=1", input),
804
805
  executorPresets: () => get("/dsar/executors/presets"),
@@ -812,6 +813,8 @@ function createCookieMunch(opts) {
812
813
  },
813
814
  taskExport: (requestId, taskId) => get(`/dsar/${enc(requestId)}/tasks/${enc(taskId)}/export`),
814
815
  task: (requestId, taskId) => get(`/dsar/${enc(requestId)}/tasks/${enc(taskId)}`),
816
+ approveTask: (requestId, taskId) => request("POST", `/dsar/${enc(requestId)}/tasks/${enc(taskId)}/approve`),
817
+ rejectTask: (requestId, taskId, o = {}) => request("POST", `/dsar/${enc(requestId)}/tasks/${enc(taskId)}/reject`, o.reason !== void 0 ? { reason: o.reason } : {}),
815
818
  testExecutor: (id, input) => request("POST", `/dsar/executors/${enc(id)}/test`, input),
816
819
  recheckExecutor: (id) => request("POST", `/dsar/executors/${enc(id)}/recheck`),
817
820
  executorHealthHistory: (id, opts2 = {}) => get(`/dsar/executors/${enc(id)}/health/history${qs({ limit: opts2.limit })}`),
@@ -851,6 +854,11 @@ function createCookieMunch(opts) {
851
854
  usage: () => get("/usage"),
852
855
  entitlements: () => get("/entitlements"),
853
856
  versions: () => get("/versions"),
857
+ anchors: {
858
+ create: (input) => request("POST", "/anchors", input),
859
+ list: (stream, query) => get(`/anchors/${enc(stream)}${qs(query)}`),
860
+ get: (stream, id) => get(`/anchors/${enc(stream)}/${enc(id)}`)
861
+ },
854
862
  sandbox: {
855
863
  get: async () => (await get("/sandbox")).sandbox,
856
864
  create: (input) => request("POST", "/sandbox", input ?? {}),
@@ -931,7 +939,7 @@ function createCookieMunch(opts) {
931
939
  delete: async (id) => {
932
940
  await request("DELETE", `/webhooks/${enc(id)}`);
933
941
  },
934
- rollSecret: (id) => request("POST", `/webhooks/${enc(id)}/roll`),
942
+ rollSecret: (id, opts2) => request("POST", `/webhooks/${enc(id)}/roll`, opts2?.secret ? { secret: opts2.secret } : void 0),
935
943
  test: (id) => request("POST", `/webhooks/${enc(id)}/test`),
936
944
  deadLetters: () => get("/webhooks/dead-letters"),
937
945
  replayDeadLetter: (id) => request("POST", `/webhooks/dead-letters/${enc(id)}/replay`),