@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.
@@ -0,0 +1,258 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/rights.ts
21
+ var rights_exports = {};
22
+ __export(rights_exports, {
23
+ createRightsHandlers: () => createRightsHandlers
24
+ });
25
+ module.exports = __toCommonJS(rights_exports);
26
+
27
+ // src/executor-conformance.ts
28
+ var CANARY_ISSUER = "https://canary.cookiemunch.net";
29
+
30
+ // src/rights.ts
31
+ var CANONICAL = {
32
+ access: "access",
33
+ export: "access",
34
+ deletion: "deletion",
35
+ erase: "deletion",
36
+ erasure: "deletion",
37
+ closure: "closure",
38
+ restriction: "restriction",
39
+ rectification: "rectification"
40
+ };
41
+ var OUTCOMES = /* @__PURE__ */ new Set(["pending", "processing", "completed", "completed_with_retention", "no_match", "rejected"]);
42
+ var CANARY_EMAIL = /^canary-([0-9a-f]{16})@canary\.cookiemunch\.net$/;
43
+ var CANARY_SUBJECT = /^canary_([0-9a-f]{16})$/;
44
+ var CANARY_EXT = /^canary:([0-9a-f]{16})$/;
45
+ var enc = new TextEncoder();
46
+ var dec = new TextDecoder();
47
+ function b64url(bytes) {
48
+ let s = "";
49
+ for (const b of bytes) s += String.fromCharCode(b);
50
+ return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
51
+ }
52
+ function fromB64url(s) {
53
+ if (!/^[A-Za-z0-9_-]*$/.test(s)) return null;
54
+ try {
55
+ const bin = atob(s.replace(/-/g, "+").replace(/_/g, "/"));
56
+ return Uint8Array.from(bin, (c) => c.charCodeAt(0));
57
+ } catch {
58
+ return null;
59
+ }
60
+ }
61
+ function sameKey(presented, expected) {
62
+ const a = enc.encode(presented);
63
+ const b = enc.encode(expected);
64
+ let diff = a.length ^ b.length;
65
+ for (let i = 0; i < b.length; i++) diff |= (a[i] ?? 0) ^ b[i];
66
+ return diff === 0;
67
+ }
68
+ function json(status, body) {
69
+ return new Response(JSON.stringify(body), {
70
+ status,
71
+ headers: { "content-type": "application/json", "x-version": "2", "cache-control": "no-store" }
72
+ });
73
+ }
74
+ var bad = (error, field) => json(400, { error, field });
75
+ var hidden = () => new Response("Not Found", { status: 404, headers: { "content-type": "text/plain" } });
76
+ function isObject(v) {
77
+ return !!v && typeof v === "object" && !Array.isArray(v);
78
+ }
79
+ function createRightsHandlers(o) {
80
+ if (typeof o.key !== "string" || !o.key) throw new Error("createRightsHandlers: key is required");
81
+ if (typeof o.idSecret !== "string" || o.idSecret.length < 32) throw new Error("createRightsHandlers: idSecret must be at least 32 characters");
82
+ if (typeof o.lookup !== "function") throw new Error("createRightsHandlers: lookup is required");
83
+ const subtle = globalThis.crypto?.subtle;
84
+ if (!subtle) throw new Error("createRightsHandlers: needs Web Crypto (globalThis.crypto.subtle)");
85
+ const store = o.store ?? /* @__PURE__ */ new Map();
86
+ const now = o.now ?? (() => Date.now());
87
+ const hmacKey = subtle.importKey("raw", enc.encode(o.idSecret), { name: "HMAC", hash: "SHA-256" }, false, ["sign", "verify"]);
88
+ async function mintId(claims) {
89
+ const payload = b64url(enc.encode(JSON.stringify(claims)));
90
+ const sig = new Uint8Array(await subtle.sign("HMAC", await hmacKey, enc.encode(payload)));
91
+ return `r1.${payload}.${b64url(sig)}`;
92
+ }
93
+ async function readId(id) {
94
+ const [v, payload, sig, extra] = id.split(".");
95
+ if (v !== "r1" || !payload || !sig || extra !== void 0) return null;
96
+ const sigBytes = fromB64url(sig);
97
+ if (!sigBytes || !await subtle.verify("HMAC", await hmacKey, sigBytes, enc.encode(payload))) return null;
98
+ const raw = fromB64url(payload);
99
+ try {
100
+ return raw ? JSON.parse(dec.decode(raw)) : null;
101
+ } catch {
102
+ return null;
103
+ }
104
+ }
105
+ const getStored = async (id) => {
106
+ const v = await store.get(`req:${id}`);
107
+ return v ? JSON.parse(v) : null;
108
+ };
109
+ const putStored = async (id, s) => {
110
+ await store.set(`req:${id}`, JSON.stringify(s));
111
+ };
112
+ const view = (id, c, s) => ({
113
+ id,
114
+ type: c.y,
115
+ canonicalType: c.t,
116
+ status: s.status,
117
+ ...s.eta ? { eta: s.eta } : {},
118
+ ...s.detail ? { detail: s.detail } : {},
119
+ ...s.retention ? { retention: s.retention } : {},
120
+ ...s.result !== void 0 ? { result: s.result } : {}
121
+ });
122
+ const callbackFor = (t) => ({ access: o.access, deletion: o.erase, closure: o.close ?? o.erase, restriction: o.restrict, rectification: o.rectify })[t];
123
+ async function act(id, c, s, cb) {
124
+ const out = CANARY_SUBJECT.test(c.s) || !cb ? void 0 : await cb({
125
+ id,
126
+ subjectId: c.s,
127
+ type: c.t,
128
+ requestedType: c.y,
129
+ caseId: c.c,
130
+ ...s.reason ? { reason: s.reason } : {},
131
+ ...c.a !== void 0 ? { executeAfter: new Date(c.a).toISOString() } : {}
132
+ });
133
+ const o2 = out ?? { status: "completed" };
134
+ if (!OUTCOMES.has(o2.status)) throw new Error(`a rights callback answered an unknown status "${String(o2.status)}"`);
135
+ const next = { status: o2.status, ...s.reason ? { reason: s.reason } : {} };
136
+ if (o2.eta) next.eta = o2.eta;
137
+ if (o2.detail) next.detail = o2.detail;
138
+ if (o2.retention) next.retention = o2.retention;
139
+ if (o2.result !== void 0) next.result = o2.result;
140
+ await putStored(id, next);
141
+ return next;
142
+ }
143
+ async function lookup(body) {
144
+ if (!Array.isArray(body.emails) || body.emails.some((e) => typeof e !== "string")) return bad("emails must be an array of strings", "emails");
145
+ const emails = body.emails.map((e) => e.trim());
146
+ const raw = body.identity ?? body.atlas;
147
+ if (raw !== void 0 && raw !== null && !(isObject(raw) && typeof raw.iss === "string" && typeof raw.sub === "string")) {
148
+ return bad("identity must be { iss, sub, ext?, principal? }", "identity");
149
+ }
150
+ const identity = isObject(raw) ? raw : void 0;
151
+ const canaryByEmail = emails.map((e) => CANARY_EMAIL.exec(e.toLowerCase())?.[1]).find(Boolean);
152
+ const canaryExt = typeof identity?.ext === "string" ? CANARY_EXT.exec(identity.ext)?.[1] : void 0;
153
+ if (canaryByEmail || canaryExt || identity?.iss === CANARY_ISSUER) {
154
+ const tag = identity ? (identity.iss === CANARY_ISSUER ? CANARY_SUBJECT.exec(identity.sub)?.[1] : void 0) ?? canaryExt : canaryByEmail;
155
+ return json(200, tag ? { subjectId: `canary_${tag}`, status: "found" } : { subjectId: null, status: "no_match" });
156
+ }
157
+ const subjectId = await o.lookup({ emails, ...identity ? { identity } : {} });
158
+ return json(200, typeof subjectId === "string" && subjectId ? { subjectId, status: "found" } : { subjectId: null, status: "no_match" });
159
+ }
160
+ async function open(body) {
161
+ const t = typeof body.type === "string" ? CANONICAL[body.type] : void 0;
162
+ if (!t) return bad("type must be one of access, export, deletion, erase, erasure, closure, restriction, rectification", "type");
163
+ if (typeof body.subjectId !== "string" || !body.subjectId) return bad("subjectId is required", "subjectId");
164
+ if (typeof body.caseId !== "string" || !body.caseId) return bad("caseId is required", "caseId");
165
+ if (body.reason !== void 0 && typeof body.reason !== "string") return bad("reason must be a string", "reason");
166
+ const after = typeof body.executeAfter === "string" ? Date.parse(body.executeAfter) : NaN;
167
+ if (body.executeAfter !== void 0 && body.executeAfter !== null && Number.isNaN(after)) return bad("executeAfter must be an ISO date-time", "executeAfter");
168
+ const cb = callbackFor(t);
169
+ if (!cb && !CANARY_SUBJECT.test(body.subjectId)) return bad(`this system does not handle ${t}`, "type");
170
+ const c = { s: body.subjectId, t, y: body.type, c: body.caseId, n: b64url(globalThis.crypto.getRandomValues(new Uint8Array(9))) };
171
+ if (!Number.isNaN(after)) c.a = after;
172
+ const id = await mintId(c);
173
+ const s = { status: "pending", ...typeof body.reason === "string" && body.reason ? { reason: body.reason } : {} };
174
+ const erasing = t === "deletion" || t === "closure";
175
+ if (erasing && c.a !== void 0 && c.a > now()) {
176
+ const scheduled = { ...s, status: "scheduled", eta: new Date(c.a).toISOString() };
177
+ await putStored(id, scheduled);
178
+ return json(201, view(id, c, scheduled));
179
+ }
180
+ return json(201, view(id, c, await act(id, c, s, cb)));
181
+ }
182
+ async function read(id, c) {
183
+ let s = await getStored(id);
184
+ if (!s) {
185
+ return json(200, view(id, c, { status: "rejected", detail: "this system no longer holds the request's state; nothing was done. Open it again." }));
186
+ }
187
+ if (s.status === "scheduled" && c.a !== void 0 && now() >= c.a) s = await act(id, c, s, callbackFor(c.t));
188
+ else if ((s.status === "pending" || s.status === "processing") && o.status && !CANARY_SUBJECT.test(c.s)) s = await act(id, c, s, o.status);
189
+ return json(200, view(id, c, s));
190
+ }
191
+ async function cancel(id, c) {
192
+ const s = await getStored(id);
193
+ if (!s) return json(409, { error: "this system no longer holds the request's state", field: "id" });
194
+ if (s.status === "cancelled") return json(200, view(id, c, s));
195
+ if (s.status !== "scheduled" && s.status !== "pending") return json(409, { error: `too late to cancel a ${s.status} request`, field: "id" });
196
+ const next = { status: "cancelled", ...s.reason ? { reason: s.reason } : {} };
197
+ await putStored(id, next);
198
+ return json(200, view(id, c, next));
199
+ }
200
+ async function handle(req) {
201
+ const m = /^Bearer\s+(.+)$/i.exec(req.headers.get("authorization") ?? "");
202
+ if (!m || !sameKey(m[1].trim(), o.key)) return hidden();
203
+ const path = new URL(req.url).pathname.replace(/\/+$/, "");
204
+ const route = /\/(lookup|requests)(?:\/([^/]+)(\/cancel)?)?$/.exec(path);
205
+ const method = req.method.toUpperCase();
206
+ if (!route) return json(404, { error: "not_found" });
207
+ const [, what, rawId, isCancel] = route;
208
+ let body = {};
209
+ let text = "";
210
+ if (method === "POST") {
211
+ text = await req.text();
212
+ try {
213
+ const parsed = text ? JSON.parse(text) : {};
214
+ if (!isObject(parsed)) return bad("body must be a JSON object", "body");
215
+ body = parsed;
216
+ } catch {
217
+ return bad("body must be JSON", "body");
218
+ }
219
+ }
220
+ const idemKey = method === "POST" && what === "requests" ? req.headers.get("idempotency-key") : null;
221
+ if (idemKey !== null) {
222
+ if (idemKey.length < 16 || idemKey.length > 128) return bad("Idempotency-Key must be 16-128 characters", "Idempotency-Key");
223
+ const seen = await store.get(`idem:${idemKey}`);
224
+ if (seen) {
225
+ const first = JSON.parse(seen);
226
+ return first.body === text ? json(first.status, first.answer) : json(409, { error: "Idempotency-Key reused with a different body", field: "Idempotency-Key" });
227
+ }
228
+ }
229
+ let res;
230
+ if (what === "lookup" && !rawId && method === "POST") res = await lookup(body);
231
+ else if (what === "requests" && !rawId && method === "POST") res = await open(body);
232
+ else if (what === "requests" && rawId) {
233
+ let id;
234
+ try {
235
+ id = decodeURIComponent(rawId);
236
+ } catch {
237
+ return json(404, { error: "unknown request" });
238
+ }
239
+ const c = await readId(id);
240
+ if (!c) return json(404, { error: "unknown request" });
241
+ if (isCancel && method === "POST") res = await cancel(id, c);
242
+ else if (!isCancel && method === "GET") res = await read(id, c);
243
+ else return json(404, { error: "not_found" });
244
+ } else return json(404, { error: "not_found" });
245
+ if (idemKey !== null && res.ok) await store.set(`idem:${idemKey}`, JSON.stringify({ body: text, status: res.status, answer: await res.clone().json() }));
246
+ return res;
247
+ }
248
+ const fetch2 = (req) => handle(req).catch((err) => {
249
+ console.error("createRightsHandlers:", err);
250
+ return json(500, { error: "internal_error" });
251
+ });
252
+ return { fetch: fetch2, GET: fetch2, POST: fetch2 };
253
+ }
254
+ // Annotate the CommonJS export names for ESM import in node:
255
+ 0 && (module.exports = {
256
+ createRightsHandlers
257
+ });
258
+ //# sourceMappingURL=rights.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/rights.ts","../src/executor-conformance.ts"],"sourcesContent":["/**\n * `@cookiemunch/sdk/rights` — the cm-standard-v2 executor contract, server side.\n *\n * A product that holds personal data implements four calls and Cookie Munch drives it (docs:\n * cookiemunch.net/docs/executor-contract-v2). This builds those four calls from callbacks, so the\n * product writes only \"find this person\", \"export them\", \"erase them\":\n *\n * import { createRightsHandlers } from '@cookiemunch/sdk/rights';\n * const rights = createRightsHandlers({\n * key: process.env.CM_EXECUTOR_KEY!, // the secretKey you registered with Cookie Munch\n * idSecret: process.env.CM_REQUEST_ID_SECRET!,\n * lookup: async ({ emails, identity }) => (await findUser(emails, identity))?.id ?? null,\n * access: async (r) => ({ status: 'completed', result: await exportUser(r.subjectId) }),\n * erase: async (r) => { await deleteUser(r.subjectId); },\n * });\n * // Next.js app/privacy/[...path]/route.ts: export const { GET, POST } = rights;\n * // Deno: Deno.serve(rights.fetch) Workers / Bun: export default rights\n *\n * Built in: a constant-time key check (no key or a wrong key is a bare 404, so the endpoint is not\n * advertised), `X-Version` and `Cache-Control: no-store` on every authorised answer, Idempotency-Key\n * replay, HMAC-signed request ids (a forged id is a 404), `executeAfter` scheduling and cancel, and\n * the canary subject, answered here so your callbacks never see it.\n *\n * A separate entry point so the API client never loads it. Web platform only (Request, Response,\n * crypto.subtle): Node 20+, Deno, Bun and edge runtimes.\n */\nimport { CANARY_ISSUER } from './executor-conformance.js';\n\nexport type RightsType = 'access' | 'deletion' | 'closure' | 'restriction' | 'rectification';\nexport type RightsStatus = 'pending' | 'scheduled' | 'processing' | 'completed' | 'completed_with_retention' | 'no_match' | 'rejected' | 'cancelled';\n\nexport interface RightsIdentity {\n iss: string;\n sub: string;\n ext?: string;\n principal?: string;\n}\n\nexport interface RightsLookupInput {\n emails: string[];\n /** A signed-in identity (sent as `identity`, or `atlas`). Match on it before the emails, and never through an email that belongs to a different identity. */\n identity?: RightsIdentity;\n}\n\nexport interface RightsRetention {\n category: string;\n basis: 'legal_obligation' | 'legal_claims' | 'contract' | 'public_interest' | 'legitimate_interest';\n until?: string;\n}\n\n/** What a callback answers. Returning nothing means `completed`. */\nexport interface RightsOutcome {\n status: 'pending' | 'processing' | 'completed' | 'completed_with_retention' | 'no_match' | 'rejected';\n eta?: string;\n /** Plain words; required in spirit for `rejected`. */\n detail?: string;\n retention?: RightsRetention[];\n /** For `access`: `{ downloadUrl, expiresAt, sha256 }`, or the bundle inline (at most 5 MB). */\n result?: unknown;\n}\n\n/** The request a callback acts on. */\nexport interface RightsRequest {\n id: string;\n subjectId: string;\n type: RightsType;\n /** The word Cookie Munch sent (`erasure`, `export`, …). */\n requestedType: string;\n caseId: string;\n reason?: string;\n executeAfter?: string;\n}\n\ntype Callback = (r: RightsRequest) => Promise<RightsOutcome | void> | RightsOutcome | void;\n\n/** Where request state and idempotency answers live. A `Map` is one; so is most of a Redis client. */\nexport interface RightsStore {\n get(key: string): Promise<string | null | undefined> | string | null | undefined;\n set(key: string, value: string): unknown;\n}\n\nexport interface RightsHandlersOptions {\n /** The key Cookie Munch sends as `Authorization: Bearer <key>`. */\n key: string;\n /** Signs request ids; at least 32 characters, the same on every instance and across restarts. */\n idSecret: string;\n /** The subject id for these emails / this identity, or null. */\n lookup: (input: RightsLookupInput) => Promise<string | null | undefined> | string | null | undefined;\n access?: Callback;\n /** `deletion` (and `closure`, when there is no `close`). Runs at `executeAfter`, not before. */\n erase?: Callback;\n close?: Callback;\n restrict?: Callback;\n rectify?: Callback;\n /** Read back a request a callback answered `pending` or `processing` for. */\n status?: Callback;\n /**\n * Default: in memory, which a restart empties. A scheduled erasure whose state is gone is never\n * run (it answers `rejected`), so give production a durable store.\n */\n store?: RightsStore;\n now?: () => number;\n}\n\nexport interface RightsHandlers {\n /** The whole contract, routed on the path's tail: mount it under any prefix. */\n fetch(req: Request): Promise<Response>;\n GET(req: Request): Promise<Response>;\n POST(req: Request): Promise<Response>;\n}\n\nconst CANONICAL: Record<string, RightsType> = {\n access: 'access', export: 'access', deletion: 'deletion', erase: 'deletion', erasure: 'deletion',\n closure: 'closure', restriction: 'restriction', rectification: 'rectification',\n};\nconst OUTCOMES = new Set(['pending', 'processing', 'completed', 'completed_with_retention', 'no_match', 'rejected']);\nconst CANARY_EMAIL = /^canary-([0-9a-f]{16})@canary\\.cookiemunch\\.net$/;\nconst CANARY_SUBJECT = /^canary_([0-9a-f]{16})$/;\nconst CANARY_EXT = /^canary:([0-9a-f]{16})$/;\n\n/** What the signed id carries: enough to answer without trusting the store for it. */\ntype IdClaims = { s: string; t: RightsType; y: string; c: string; a?: number; n: string };\n/** The request's mutable state, in the store under `req:<id>`. */\ntype Stored = { status: RightsStatus; eta?: string; detail?: string; retention?: RightsRetention[]; result?: unknown; reason?: string };\n\nconst enc = new TextEncoder();\nconst dec = new TextDecoder();\n\nfunction b64url(bytes: Uint8Array): string {\n let s = '';\n for (const b of bytes) s += String.fromCharCode(b);\n return btoa(s).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n}\nfunction fromB64url(s: string) {\n if (!/^[A-Za-z0-9_-]*$/.test(s)) return null;\n try {\n const bin = atob(s.replace(/-/g, '+').replace(/_/g, '/'));\n return Uint8Array.from(bin, (c) => c.charCodeAt(0));\n } catch {\n return null;\n }\n}\n\n/** Constant-time in the presented value: every byte of the expected key is compared. */\nfunction sameKey(presented: string, expected: string): boolean {\n const a = enc.encode(presented);\n const b = enc.encode(expected);\n let diff = a.length ^ b.length;\n for (let i = 0; i < b.length; i++) diff |= (a[i] ?? 0) ^ b[i]!;\n return diff === 0;\n}\n\nfunction json(status: number, body: unknown): Response {\n return new Response(JSON.stringify(body), {\n status,\n headers: { 'content-type': 'application/json', 'x-version': '2', 'cache-control': 'no-store' },\n });\n}\nconst bad = (error: string, field: string) => json(400, { error, field });\n// No X-Version and nothing contract-shaped: to a caller without the key this is any other 404.\nconst hidden = () => new Response('Not Found', { status: 404, headers: { 'content-type': 'text/plain' } });\n\nfunction isObject(v: unknown): v is Record<string, unknown> {\n return !!v && typeof v === 'object' && !Array.isArray(v);\n}\n\nexport function createRightsHandlers(o: RightsHandlersOptions): RightsHandlers {\n if (typeof o.key !== 'string' || !o.key) throw new Error('createRightsHandlers: key is required');\n if (typeof o.idSecret !== 'string' || o.idSecret.length < 32) throw new Error('createRightsHandlers: idSecret must be at least 32 characters');\n if (typeof o.lookup !== 'function') throw new Error('createRightsHandlers: lookup is required');\n const subtle = globalThis.crypto?.subtle;\n if (!subtle) throw new Error('createRightsHandlers: needs Web Crypto (globalThis.crypto.subtle)');\n // shortcut: the default store never forgets an entry; fine for the volume of rights requests, pass `store` for more.\n const store: RightsStore = o.store ?? new Map<string, string>();\n const now = o.now ?? (() => Date.now());\n const hmacKey = subtle.importKey('raw', enc.encode(o.idSecret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign', 'verify']);\n\n async function mintId(claims: IdClaims): Promise<string> {\n const payload = b64url(enc.encode(JSON.stringify(claims)));\n const sig = new Uint8Array(await subtle.sign('HMAC', await hmacKey, enc.encode(payload)));\n return `r1.${payload}.${b64url(sig)}`;\n }\n async function readId(id: string): Promise<IdClaims | null> {\n const [v, payload, sig, extra] = id.split('.');\n if (v !== 'r1' || !payload || !sig || extra !== undefined) return null;\n const sigBytes = fromB64url(sig);\n if (!sigBytes || !(await subtle.verify('HMAC', await hmacKey, sigBytes, enc.encode(payload)))) return null;\n const raw = fromB64url(payload);\n try {\n return raw ? (JSON.parse(dec.decode(raw)) as IdClaims) : null;\n } catch {\n return null;\n }\n }\n\n const getStored = async (id: string): Promise<Stored | null> => {\n const v = await store.get(`req:${id}`);\n return v ? (JSON.parse(v) as Stored) : null;\n };\n const putStored = async (id: string, s: Stored) => {\n await store.set(`req:${id}`, JSON.stringify(s));\n };\n\n const view = (id: string, c: IdClaims, s: Stored) => ({\n id,\n type: c.y,\n canonicalType: c.t,\n status: s.status,\n ...(s.eta ? { eta: s.eta } : {}),\n ...(s.detail ? { detail: s.detail } : {}),\n ...(s.retention ? { retention: s.retention } : {}),\n ...(s.result !== undefined ? { result: s.result } : {}),\n });\n\n const callbackFor = (t: RightsType): Callback | undefined =>\n ({ access: o.access, deletion: o.erase, closure: o.close ?? o.erase, restriction: o.restrict, rectification: o.rectify })[t];\n\n /** Run a callback (or, for the canary, nothing) and fold its answer into the stored state. */\n async function act(id: string, c: IdClaims, s: Stored, cb: Callback | undefined): Promise<Stored> {\n // The canary is a synthetic person: requests on it complete without touching anything real.\n const out = CANARY_SUBJECT.test(c.s) || !cb\n ? undefined\n : ((await cb({\n id,\n subjectId: c.s,\n type: c.t,\n requestedType: c.y,\n caseId: c.c,\n ...(s.reason ? { reason: s.reason } : {}),\n ...(c.a !== undefined ? { executeAfter: new Date(c.a).toISOString() } : {}),\n })) as RightsOutcome | undefined);\n const o2: RightsOutcome = out ?? { status: 'completed' };\n if (!OUTCOMES.has(o2.status)) throw new Error(`a rights callback answered an unknown status \"${String(o2.status)}\"`);\n const next: Stored = { status: o2.status, ...(s.reason ? { reason: s.reason } : {}) };\n if (o2.eta) next.eta = o2.eta;\n if (o2.detail) next.detail = o2.detail;\n if (o2.retention) next.retention = o2.retention;\n if (o2.result !== undefined) next.result = o2.result;\n await putStored(id, next);\n return next;\n }\n\n async function lookup(body: Record<string, unknown>): Promise<Response> {\n if (!Array.isArray(body.emails) || body.emails.some((e) => typeof e !== 'string')) return bad('emails must be an array of strings', 'emails');\n const emails = (body.emails as string[]).map((e) => e.trim());\n const raw = body.identity ?? body.atlas;\n if (raw !== undefined && raw !== null && !(isObject(raw) && typeof raw.iss === 'string' && typeof raw.sub === 'string')) {\n return bad('identity must be { iss, sub, ext?, principal? }', 'identity');\n }\n const identity = isObject(raw) ? (raw as unknown as RightsIdentity) : undefined;\n\n const canaryByEmail = emails.map((e) => CANARY_EMAIL.exec(e.toLowerCase())?.[1]).find(Boolean);\n const canaryExt = typeof identity?.ext === 'string' ? CANARY_EXT.exec(identity.ext)?.[1] : undefined;\n if (canaryByEmail || canaryExt || identity?.iss === CANARY_ISSUER) {\n // When an identity is given only it decides: the canary's email next to anyone else's identity is no one.\n const tag = identity ? (identity.iss === CANARY_ISSUER ? CANARY_SUBJECT.exec(identity.sub)?.[1] : undefined) ?? canaryExt : canaryByEmail;\n return json(200, tag ? { subjectId: `canary_${tag}`, status: 'found' } : { subjectId: null, status: 'no_match' });\n }\n const subjectId = await o.lookup({ emails, ...(identity ? { identity } : {}) });\n return json(200, typeof subjectId === 'string' && subjectId ? { subjectId, status: 'found' } : { subjectId: null, status: 'no_match' });\n }\n\n async function open(body: Record<string, unknown>): Promise<Response> {\n const t = typeof body.type === 'string' ? CANONICAL[body.type] : undefined;\n if (!t) return bad('type must be one of access, export, deletion, erase, erasure, closure, restriction, rectification', 'type');\n if (typeof body.subjectId !== 'string' || !body.subjectId) return bad('subjectId is required', 'subjectId');\n if (typeof body.caseId !== 'string' || !body.caseId) return bad('caseId is required', 'caseId');\n if (body.reason !== undefined && typeof body.reason !== 'string') return bad('reason must be a string', 'reason');\n const after = typeof body.executeAfter === 'string' ? Date.parse(body.executeAfter) : NaN;\n if (body.executeAfter !== undefined && body.executeAfter !== null && Number.isNaN(after)) return bad('executeAfter must be an ISO date-time', 'executeAfter');\n const cb = callbackFor(t);\n if (!cb && !CANARY_SUBJECT.test(body.subjectId)) return bad(`this system does not handle ${t}`, 'type');\n\n const c: IdClaims = { s: body.subjectId, t, y: body.type as string, c: body.caseId, n: b64url(globalThis.crypto.getRandomValues(new Uint8Array(9))) };\n if (!Number.isNaN(after)) c.a = after;\n const id = await mintId(c);\n const s: Stored = { status: 'pending', ...(typeof body.reason === 'string' && body.reason ? { reason: body.reason } : {}) };\n const erasing = t === 'deletion' || t === 'closure';\n if (erasing && c.a !== undefined && c.a > now()) {\n const scheduled: Stored = { ...s, status: 'scheduled', eta: new Date(c.a).toISOString() };\n await putStored(id, scheduled);\n return json(201, view(id, c, scheduled));\n }\n return json(201, view(id, c, await act(id, c, s, cb)));\n }\n\n async function read(id: string, c: IdClaims): Promise<Response> {\n let s = await getStored(id);\n if (!s) {\n // A genuine id whose state is gone (an emptied store): never act on it, say so.\n return json(200, view(id, c, { status: 'rejected', detail: \"this system no longer holds the request's state; nothing was done. Open it again.\" }));\n }\n if (s.status === 'scheduled' && c.a !== undefined && now() >= c.a) s = await act(id, c, s, callbackFor(c.t));\n else if ((s.status === 'pending' || s.status === 'processing') && o.status && !CANARY_SUBJECT.test(c.s)) s = await act(id, c, s, o.status);\n return json(200, view(id, c, s));\n }\n\n async function cancel(id: string, c: IdClaims): Promise<Response> {\n const s = await getStored(id);\n if (!s) return json(409, { error: \"this system no longer holds the request's state\", field: 'id' });\n if (s.status === 'cancelled') return json(200, view(id, c, s));\n if (s.status !== 'scheduled' && s.status !== 'pending') return json(409, { error: `too late to cancel a ${s.status} request`, field: 'id' });\n const next: Stored = { status: 'cancelled', ...(s.reason ? { reason: s.reason } : {}) };\n await putStored(id, next);\n return json(200, view(id, c, next));\n }\n\n async function handle(req: Request): Promise<Response> {\n const m = /^Bearer\\s+(.+)$/i.exec(req.headers.get('authorization') ?? '');\n if (!m || !sameKey(m[1]!.trim(), o.key)) return hidden();\n\n const path = new URL(req.url).pathname.replace(/\\/+$/, '');\n const route = /\\/(lookup|requests)(?:\\/([^/]+)(\\/cancel)?)?$/.exec(path);\n const method = req.method.toUpperCase();\n if (!route) return json(404, { error: 'not_found' });\n const [, what, rawId, isCancel] = route;\n\n let body: Record<string, unknown> = {};\n let text = '';\n if (method === 'POST') {\n text = await req.text();\n try {\n const parsed: unknown = text ? JSON.parse(text) : {};\n if (!isObject(parsed)) return bad('body must be a JSON object', 'body');\n body = parsed;\n } catch {\n return bad('body must be JSON', 'body');\n }\n }\n\n // Replay: the same key and body answer what they answered first; another body is a 409.\n const idemKey = method === 'POST' && what === 'requests' ? req.headers.get('idempotency-key') : null;\n if (idemKey !== null) {\n if (idemKey.length < 16 || idemKey.length > 128) return bad('Idempotency-Key must be 16-128 characters', 'Idempotency-Key');\n const seen = await store.get(`idem:${idemKey}`);\n if (seen) {\n const first = JSON.parse(seen) as { body: string; status: number; answer: unknown };\n return first.body === text ? json(first.status, first.answer) : json(409, { error: 'Idempotency-Key reused with a different body', field: 'Idempotency-Key' });\n }\n }\n\n let res: Response;\n if (what === 'lookup' && !rawId && method === 'POST') res = await lookup(body);\n else if (what === 'requests' && !rawId && method === 'POST') res = await open(body);\n else if (what === 'requests' && rawId) {\n let id: string;\n try {\n id = decodeURIComponent(rawId);\n } catch {\n return json(404, { error: 'unknown request' });\n }\n const c = await readId(id);\n if (!c) return json(404, { error: 'unknown request' });\n if (isCancel && method === 'POST') res = await cancel(id, c);\n else if (!isCancel && method === 'GET') res = await read(id, c);\n else return json(404, { error: 'not_found' });\n } else return json(404, { error: 'not_found' });\n\n if (idemKey !== null && res.ok) await store.set(`idem:${idemKey}`, JSON.stringify({ body: text, status: res.status, answer: await res.clone().json() }));\n return res;\n }\n\n // A throwing callback is a 500 Cookie Munch retries; nothing is recorded for it.\n const fetch = (req: Request) =>\n handle(req).catch((err: unknown) => {\n console.error('createRightsHandlers:', err);\n return json(500, { error: 'internal_error' });\n });\n return { fetch, GET: fetch, POST: fetch };\n}\n","/**\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":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;AC+DO,IAAM,gBAAgB;;;ADgD7B,IAAM,YAAwC;AAAA,EAC5C,QAAQ;AAAA,EAAU,QAAQ;AAAA,EAAU,UAAU;AAAA,EAAY,OAAO;AAAA,EAAY,SAAS;AAAA,EACtF,SAAS;AAAA,EAAW,aAAa;AAAA,EAAe,eAAe;AACjE;AACA,IAAM,WAAW,oBAAI,IAAI,CAAC,WAAW,cAAc,aAAa,4BAA4B,YAAY,UAAU,CAAC;AACnH,IAAM,eAAe;AACrB,IAAM,iBAAiB;AACvB,IAAM,aAAa;AAOnB,IAAM,MAAM,IAAI,YAAY;AAC5B,IAAM,MAAM,IAAI,YAAY;AAE5B,SAAS,OAAO,OAA2B;AACzC,MAAI,IAAI;AACR,aAAW,KAAK,MAAO,MAAK,OAAO,aAAa,CAAC;AACjD,SAAO,KAAK,CAAC,EAAE,QAAQ,OAAO,GAAG,EAAE,QAAQ,OAAO,GAAG,EAAE,QAAQ,OAAO,EAAE;AAC1E;AACA,SAAS,WAAW,GAAW;AAC7B,MAAI,CAAC,mBAAmB,KAAK,CAAC,EAAG,QAAO;AACxC,MAAI;AACF,UAAM,MAAM,KAAK,EAAE,QAAQ,MAAM,GAAG,EAAE,QAAQ,MAAM,GAAG,CAAC;AACxD,WAAO,WAAW,KAAK,KAAK,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;AAAA,EACpD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGA,SAAS,QAAQ,WAAmB,UAA2B;AAC7D,QAAM,IAAI,IAAI,OAAO,SAAS;AAC9B,QAAM,IAAI,IAAI,OAAO,QAAQ;AAC7B,MAAI,OAAO,EAAE,SAAS,EAAE;AACxB,WAAS,IAAI,GAAG,IAAI,EAAE,QAAQ,IAAK,UAAS,EAAE,CAAC,KAAK,KAAK,EAAE,CAAC;AAC5D,SAAO,SAAS;AAClB;AAEA,SAAS,KAAK,QAAgB,MAAyB;AACrD,SAAO,IAAI,SAAS,KAAK,UAAU,IAAI,GAAG;AAAA,IACxC;AAAA,IACA,SAAS,EAAE,gBAAgB,oBAAoB,aAAa,KAAK,iBAAiB,WAAW;AAAA,EAC/F,CAAC;AACH;AACA,IAAM,MAAM,CAAC,OAAe,UAAkB,KAAK,KAAK,EAAE,OAAO,MAAM,CAAC;AAExE,IAAM,SAAS,MAAM,IAAI,SAAS,aAAa,EAAE,QAAQ,KAAK,SAAS,EAAE,gBAAgB,aAAa,EAAE,CAAC;AAEzG,SAAS,SAAS,GAA0C;AAC1D,SAAO,CAAC,CAAC,KAAK,OAAO,MAAM,YAAY,CAAC,MAAM,QAAQ,CAAC;AACzD;AAEO,SAAS,qBAAqB,GAA0C;AAC7E,MAAI,OAAO,EAAE,QAAQ,YAAY,CAAC,EAAE,IAAK,OAAM,IAAI,MAAM,uCAAuC;AAChG,MAAI,OAAO,EAAE,aAAa,YAAY,EAAE,SAAS,SAAS,GAAI,OAAM,IAAI,MAAM,+DAA+D;AAC7I,MAAI,OAAO,EAAE,WAAW,WAAY,OAAM,IAAI,MAAM,0CAA0C;AAC9F,QAAM,SAAS,WAAW,QAAQ;AAClC,MAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,mEAAmE;AAEhG,QAAM,QAAqB,EAAE,SAAS,oBAAI,IAAoB;AAC9D,QAAM,MAAM,EAAE,QAAQ,MAAM,KAAK,IAAI;AACrC,QAAM,UAAU,OAAO,UAAU,OAAO,IAAI,OAAO,EAAE,QAAQ,GAAG,EAAE,MAAM,QAAQ,MAAM,UAAU,GAAG,OAAO,CAAC,QAAQ,QAAQ,CAAC;AAE5H,iBAAe,OAAO,QAAmC;AACvD,UAAM,UAAU,OAAO,IAAI,OAAO,KAAK,UAAU,MAAM,CAAC,CAAC;AACzD,UAAM,MAAM,IAAI,WAAW,MAAM,OAAO,KAAK,QAAQ,MAAM,SAAS,IAAI,OAAO,OAAO,CAAC,CAAC;AACxF,WAAO,MAAM,OAAO,IAAI,OAAO,GAAG,CAAC;AAAA,EACrC;AACA,iBAAe,OAAO,IAAsC;AAC1D,UAAM,CAAC,GAAG,SAAS,KAAK,KAAK,IAAI,GAAG,MAAM,GAAG;AAC7C,QAAI,MAAM,QAAQ,CAAC,WAAW,CAAC,OAAO,UAAU,OAAW,QAAO;AAClE,UAAM,WAAW,WAAW,GAAG;AAC/B,QAAI,CAAC,YAAY,CAAE,MAAM,OAAO,OAAO,QAAQ,MAAM,SAAS,UAAU,IAAI,OAAO,OAAO,CAAC,EAAI,QAAO;AACtG,UAAM,MAAM,WAAW,OAAO;AAC9B,QAAI;AACF,aAAO,MAAO,KAAK,MAAM,IAAI,OAAO,GAAG,CAAC,IAAiB;AAAA,IAC3D,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAEA,QAAM,YAAY,OAAO,OAAuC;AAC9D,UAAM,IAAI,MAAM,MAAM,IAAI,OAAO,EAAE,EAAE;AACrC,WAAO,IAAK,KAAK,MAAM,CAAC,IAAe;AAAA,EACzC;AACA,QAAM,YAAY,OAAO,IAAY,MAAc;AACjD,UAAM,MAAM,IAAI,OAAO,EAAE,IAAI,KAAK,UAAU,CAAC,CAAC;AAAA,EAChD;AAEA,QAAM,OAAO,CAAC,IAAY,GAAa,OAAe;AAAA,IACpD;AAAA,IACA,MAAM,EAAE;AAAA,IACR,eAAe,EAAE;AAAA,IACjB,QAAQ,EAAE;AAAA,IACV,GAAI,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,IAAI,CAAC;AAAA,IAC9B,GAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,OAAO,IAAI,CAAC;AAAA,IACvC,GAAI,EAAE,YAAY,EAAE,WAAW,EAAE,UAAU,IAAI,CAAC;AAAA,IAChD,GAAI,EAAE,WAAW,SAAY,EAAE,QAAQ,EAAE,OAAO,IAAI,CAAC;AAAA,EACvD;AAEA,QAAM,cAAc,CAAC,OAClB,EAAE,QAAQ,EAAE,QAAQ,UAAU,EAAE,OAAO,SAAS,EAAE,SAAS,EAAE,OAAO,aAAa,EAAE,UAAU,eAAe,EAAE,QAAQ,GAAG,CAAC;AAG7H,iBAAe,IAAI,IAAY,GAAa,GAAW,IAA2C;AAEhG,UAAM,MAAM,eAAe,KAAK,EAAE,CAAC,KAAK,CAAC,KACrC,SACE,MAAM,GAAG;AAAA,MACT;AAAA,MACA,WAAW,EAAE;AAAA,MACb,MAAM,EAAE;AAAA,MACR,eAAe,EAAE;AAAA,MACjB,QAAQ,EAAE;AAAA,MACV,GAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,OAAO,IAAI,CAAC;AAAA,MACvC,GAAI,EAAE,MAAM,SAAY,EAAE,cAAc,IAAI,KAAK,EAAE,CAAC,EAAE,YAAY,EAAE,IAAI,CAAC;AAAA,IAC3E,CAAC;AACL,UAAM,KAAoB,OAAO,EAAE,QAAQ,YAAY;AACvD,QAAI,CAAC,SAAS,IAAI,GAAG,MAAM,EAAG,OAAM,IAAI,MAAM,iDAAiD,OAAO,GAAG,MAAM,CAAC,GAAG;AACnH,UAAM,OAAe,EAAE,QAAQ,GAAG,QAAQ,GAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,OAAO,IAAI,CAAC,EAAG;AACpF,QAAI,GAAG,IAAK,MAAK,MAAM,GAAG;AAC1B,QAAI,GAAG,OAAQ,MAAK,SAAS,GAAG;AAChC,QAAI,GAAG,UAAW,MAAK,YAAY,GAAG;AACtC,QAAI,GAAG,WAAW,OAAW,MAAK,SAAS,GAAG;AAC9C,UAAM,UAAU,IAAI,IAAI;AACxB,WAAO;AAAA,EACT;AAEA,iBAAe,OAAO,MAAkD;AACtE,QAAI,CAAC,MAAM,QAAQ,KAAK,MAAM,KAAK,KAAK,OAAO,KAAK,CAAC,MAAM,OAAO,MAAM,QAAQ,EAAG,QAAO,IAAI,sCAAsC,QAAQ;AAC5I,UAAM,SAAU,KAAK,OAAoB,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC;AAC5D,UAAM,MAAM,KAAK,YAAY,KAAK;AAClC,QAAI,QAAQ,UAAa,QAAQ,QAAQ,EAAE,SAAS,GAAG,KAAK,OAAO,IAAI,QAAQ,YAAY,OAAO,IAAI,QAAQ,WAAW;AACvH,aAAO,IAAI,mDAAmD,UAAU;AAAA,IAC1E;AACA,UAAM,WAAW,SAAS,GAAG,IAAK,MAAoC;AAEtE,UAAM,gBAAgB,OAAO,IAAI,CAAC,MAAM,aAAa,KAAK,EAAE,YAAY,CAAC,IAAI,CAAC,CAAC,EAAE,KAAK,OAAO;AAC7F,UAAM,YAAY,OAAO,UAAU,QAAQ,WAAW,WAAW,KAAK,SAAS,GAAG,IAAI,CAAC,IAAI;AAC3F,QAAI,iBAAiB,aAAa,UAAU,QAAQ,eAAe;AAEjE,YAAM,MAAM,YAAY,SAAS,QAAQ,gBAAgB,eAAe,KAAK,SAAS,GAAG,IAAI,CAAC,IAAI,WAAc,YAAY;AAC5H,aAAO,KAAK,KAAK,MAAM,EAAE,WAAW,UAAU,GAAG,IAAI,QAAQ,QAAQ,IAAI,EAAE,WAAW,MAAM,QAAQ,WAAW,CAAC;AAAA,IAClH;AACA,UAAM,YAAY,MAAM,EAAE,OAAO,EAAE,QAAQ,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC,EAAG,CAAC;AAC9E,WAAO,KAAK,KAAK,OAAO,cAAc,YAAY,YAAY,EAAE,WAAW,QAAQ,QAAQ,IAAI,EAAE,WAAW,MAAM,QAAQ,WAAW,CAAC;AAAA,EACxI;AAEA,iBAAe,KAAK,MAAkD;AACpE,UAAM,IAAI,OAAO,KAAK,SAAS,WAAW,UAAU,KAAK,IAAI,IAAI;AACjE,QAAI,CAAC,EAAG,QAAO,IAAI,qGAAqG,MAAM;AAC9H,QAAI,OAAO,KAAK,cAAc,YAAY,CAAC,KAAK,UAAW,QAAO,IAAI,yBAAyB,WAAW;AAC1G,QAAI,OAAO,KAAK,WAAW,YAAY,CAAC,KAAK,OAAQ,QAAO,IAAI,sBAAsB,QAAQ;AAC9F,QAAI,KAAK,WAAW,UAAa,OAAO,KAAK,WAAW,SAAU,QAAO,IAAI,2BAA2B,QAAQ;AAChH,UAAM,QAAQ,OAAO,KAAK,iBAAiB,WAAW,KAAK,MAAM,KAAK,YAAY,IAAI;AACtF,QAAI,KAAK,iBAAiB,UAAa,KAAK,iBAAiB,QAAQ,OAAO,MAAM,KAAK,EAAG,QAAO,IAAI,yCAAyC,cAAc;AAC5J,UAAM,KAAK,YAAY,CAAC;AACxB,QAAI,CAAC,MAAM,CAAC,eAAe,KAAK,KAAK,SAAS,EAAG,QAAO,IAAI,+BAA+B,CAAC,IAAI,MAAM;AAEtG,UAAM,IAAc,EAAE,GAAG,KAAK,WAAW,GAAG,GAAG,KAAK,MAAgB,GAAG,KAAK,QAAQ,GAAG,OAAO,WAAW,OAAO,gBAAgB,IAAI,WAAW,CAAC,CAAC,CAAC,EAAE;AACpJ,QAAI,CAAC,OAAO,MAAM,KAAK,EAAG,GAAE,IAAI;AAChC,UAAM,KAAK,MAAM,OAAO,CAAC;AACzB,UAAM,IAAY,EAAE,QAAQ,WAAW,GAAI,OAAO,KAAK,WAAW,YAAY,KAAK,SAAS,EAAE,QAAQ,KAAK,OAAO,IAAI,CAAC,EAAG;AAC1H,UAAM,UAAU,MAAM,cAAc,MAAM;AAC1C,QAAI,WAAW,EAAE,MAAM,UAAa,EAAE,IAAI,IAAI,GAAG;AAC/C,YAAM,YAAoB,EAAE,GAAG,GAAG,QAAQ,aAAa,KAAK,IAAI,KAAK,EAAE,CAAC,EAAE,YAAY,EAAE;AACxF,YAAM,UAAU,IAAI,SAAS;AAC7B,aAAO,KAAK,KAAK,KAAK,IAAI,GAAG,SAAS,CAAC;AAAA,IACzC;AACA,WAAO,KAAK,KAAK,KAAK,IAAI,GAAG,MAAM,IAAI,IAAI,GAAG,GAAG,EAAE,CAAC,CAAC;AAAA,EACvD;AAEA,iBAAe,KAAK,IAAY,GAAgC;AAC9D,QAAI,IAAI,MAAM,UAAU,EAAE;AAC1B,QAAI,CAAC,GAAG;AAEN,aAAO,KAAK,KAAK,KAAK,IAAI,GAAG,EAAE,QAAQ,YAAY,QAAQ,oFAAoF,CAAC,CAAC;AAAA,IACnJ;AACA,QAAI,EAAE,WAAW,eAAe,EAAE,MAAM,UAAa,IAAI,KAAK,EAAE,EAAG,KAAI,MAAM,IAAI,IAAI,GAAG,GAAG,YAAY,EAAE,CAAC,CAAC;AAAA,cACjG,EAAE,WAAW,aAAa,EAAE,WAAW,iBAAiB,EAAE,UAAU,CAAC,eAAe,KAAK,EAAE,CAAC,EAAG,KAAI,MAAM,IAAI,IAAI,GAAG,GAAG,EAAE,MAAM;AACzI,WAAO,KAAK,KAAK,KAAK,IAAI,GAAG,CAAC,CAAC;AAAA,EACjC;AAEA,iBAAe,OAAO,IAAY,GAAgC;AAChE,UAAM,IAAI,MAAM,UAAU,EAAE;AAC5B,QAAI,CAAC,EAAG,QAAO,KAAK,KAAK,EAAE,OAAO,mDAAmD,OAAO,KAAK,CAAC;AAClG,QAAI,EAAE,WAAW,YAAa,QAAO,KAAK,KAAK,KAAK,IAAI,GAAG,CAAC,CAAC;AAC7D,QAAI,EAAE,WAAW,eAAe,EAAE,WAAW,UAAW,QAAO,KAAK,KAAK,EAAE,OAAO,wBAAwB,EAAE,MAAM,YAAY,OAAO,KAAK,CAAC;AAC3I,UAAM,OAAe,EAAE,QAAQ,aAAa,GAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,OAAO,IAAI,CAAC,EAAG;AACtF,UAAM,UAAU,IAAI,IAAI;AACxB,WAAO,KAAK,KAAK,KAAK,IAAI,GAAG,IAAI,CAAC;AAAA,EACpC;AAEA,iBAAe,OAAO,KAAiC;AACrD,UAAM,IAAI,mBAAmB,KAAK,IAAI,QAAQ,IAAI,eAAe,KAAK,EAAE;AACxE,QAAI,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC,EAAG,KAAK,GAAG,EAAE,GAAG,EAAG,QAAO,OAAO;AAEvD,UAAM,OAAO,IAAI,IAAI,IAAI,GAAG,EAAE,SAAS,QAAQ,QAAQ,EAAE;AACzD,UAAM,QAAQ,gDAAgD,KAAK,IAAI;AACvE,UAAM,SAAS,IAAI,OAAO,YAAY;AACtC,QAAI,CAAC,MAAO,QAAO,KAAK,KAAK,EAAE,OAAO,YAAY,CAAC;AACnD,UAAM,CAAC,EAAE,MAAM,OAAO,QAAQ,IAAI;AAElC,QAAI,OAAgC,CAAC;AACrC,QAAI,OAAO;AACX,QAAI,WAAW,QAAQ;AACrB,aAAO,MAAM,IAAI,KAAK;AACtB,UAAI;AACF,cAAM,SAAkB,OAAO,KAAK,MAAM,IAAI,IAAI,CAAC;AACnD,YAAI,CAAC,SAAS,MAAM,EAAG,QAAO,IAAI,8BAA8B,MAAM;AACtE,eAAO;AAAA,MACT,QAAQ;AACN,eAAO,IAAI,qBAAqB,MAAM;AAAA,MACxC;AAAA,IACF;AAGA,UAAM,UAAU,WAAW,UAAU,SAAS,aAAa,IAAI,QAAQ,IAAI,iBAAiB,IAAI;AAChG,QAAI,YAAY,MAAM;AACpB,UAAI,QAAQ,SAAS,MAAM,QAAQ,SAAS,IAAK,QAAO,IAAI,6CAA6C,iBAAiB;AAC1H,YAAM,OAAO,MAAM,MAAM,IAAI,QAAQ,OAAO,EAAE;AAC9C,UAAI,MAAM;AACR,cAAM,QAAQ,KAAK,MAAM,IAAI;AAC7B,eAAO,MAAM,SAAS,OAAO,KAAK,MAAM,QAAQ,MAAM,MAAM,IAAI,KAAK,KAAK,EAAE,OAAO,gDAAgD,OAAO,kBAAkB,CAAC;AAAA,MAC/J;AAAA,IACF;AAEA,QAAI;AACJ,QAAI,SAAS,YAAY,CAAC,SAAS,WAAW,OAAQ,OAAM,MAAM,OAAO,IAAI;AAAA,aACpE,SAAS,cAAc,CAAC,SAAS,WAAW,OAAQ,OAAM,MAAM,KAAK,IAAI;AAAA,aACzE,SAAS,cAAc,OAAO;AACrC,UAAI;AACJ,UAAI;AACF,aAAK,mBAAmB,KAAK;AAAA,MAC/B,QAAQ;AACN,eAAO,KAAK,KAAK,EAAE,OAAO,kBAAkB,CAAC;AAAA,MAC/C;AACA,YAAM,IAAI,MAAM,OAAO,EAAE;AACzB,UAAI,CAAC,EAAG,QAAO,KAAK,KAAK,EAAE,OAAO,kBAAkB,CAAC;AACrD,UAAI,YAAY,WAAW,OAAQ,OAAM,MAAM,OAAO,IAAI,CAAC;AAAA,eAClD,CAAC,YAAY,WAAW,MAAO,OAAM,MAAM,KAAK,IAAI,CAAC;AAAA,UACzD,QAAO,KAAK,KAAK,EAAE,OAAO,YAAY,CAAC;AAAA,IAC9C,MAAO,QAAO,KAAK,KAAK,EAAE,OAAO,YAAY,CAAC;AAE9C,QAAI,YAAY,QAAQ,IAAI,GAAI,OAAM,MAAM,IAAI,QAAQ,OAAO,IAAI,KAAK,UAAU,EAAE,MAAM,MAAM,QAAQ,IAAI,QAAQ,QAAQ,MAAM,IAAI,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;AACvJ,WAAO;AAAA,EACT;AAGA,QAAMA,SAAQ,CAAC,QACb,OAAO,GAAG,EAAE,MAAM,CAAC,QAAiB;AAClC,YAAQ,MAAM,yBAAyB,GAAG;AAC1C,WAAO,KAAK,KAAK,EAAE,OAAO,iBAAiB,CAAC;AAAA,EAC9C,CAAC;AACH,SAAO,EAAE,OAAAA,QAAO,KAAKA,QAAO,MAAMA,OAAM;AAC1C;","names":["fetch"]}
@@ -0,0 +1,75 @@
1
+ export type RightsType = 'access' | 'deletion' | 'closure' | 'restriction' | 'rectification';
2
+ export type RightsStatus = 'pending' | 'scheduled' | 'processing' | 'completed' | 'completed_with_retention' | 'no_match' | 'rejected' | 'cancelled';
3
+ export interface RightsIdentity {
4
+ iss: string;
5
+ sub: string;
6
+ ext?: string;
7
+ principal?: string;
8
+ }
9
+ export interface RightsLookupInput {
10
+ emails: string[];
11
+ /** A signed-in identity (sent as `identity`, or `atlas`). Match on it before the emails, and never through an email that belongs to a different identity. */
12
+ identity?: RightsIdentity;
13
+ }
14
+ export interface RightsRetention {
15
+ category: string;
16
+ basis: 'legal_obligation' | 'legal_claims' | 'contract' | 'public_interest' | 'legitimate_interest';
17
+ until?: string;
18
+ }
19
+ /** What a callback answers. Returning nothing means `completed`. */
20
+ export interface RightsOutcome {
21
+ status: 'pending' | 'processing' | 'completed' | 'completed_with_retention' | 'no_match' | 'rejected';
22
+ eta?: string;
23
+ /** Plain words; required in spirit for `rejected`. */
24
+ detail?: string;
25
+ retention?: RightsRetention[];
26
+ /** For `access`: `{ downloadUrl, expiresAt, sha256 }`, or the bundle inline (at most 5 MB). */
27
+ result?: unknown;
28
+ }
29
+ /** The request a callback acts on. */
30
+ export interface RightsRequest {
31
+ id: string;
32
+ subjectId: string;
33
+ type: RightsType;
34
+ /** The word Cookie Munch sent (`erasure`, `export`, …). */
35
+ requestedType: string;
36
+ caseId: string;
37
+ reason?: string;
38
+ executeAfter?: string;
39
+ }
40
+ type Callback = (r: RightsRequest) => Promise<RightsOutcome | void> | RightsOutcome | void;
41
+ /** Where request state and idempotency answers live. A `Map` is one; so is most of a Redis client. */
42
+ export interface RightsStore {
43
+ get(key: string): Promise<string | null | undefined> | string | null | undefined;
44
+ set(key: string, value: string): unknown;
45
+ }
46
+ export interface RightsHandlersOptions {
47
+ /** The key Cookie Munch sends as `Authorization: Bearer <key>`. */
48
+ key: string;
49
+ /** Signs request ids; at least 32 characters, the same on every instance and across restarts. */
50
+ idSecret: string;
51
+ /** The subject id for these emails / this identity, or null. */
52
+ lookup: (input: RightsLookupInput) => Promise<string | null | undefined> | string | null | undefined;
53
+ access?: Callback;
54
+ /** `deletion` (and `closure`, when there is no `close`). Runs at `executeAfter`, not before. */
55
+ erase?: Callback;
56
+ close?: Callback;
57
+ restrict?: Callback;
58
+ rectify?: Callback;
59
+ /** Read back a request a callback answered `pending` or `processing` for. */
60
+ status?: Callback;
61
+ /**
62
+ * Default: in memory, which a restart empties. A scheduled erasure whose state is gone is never
63
+ * run (it answers `rejected`), so give production a durable store.
64
+ */
65
+ store?: RightsStore;
66
+ now?: () => number;
67
+ }
68
+ export interface RightsHandlers {
69
+ /** The whole contract, routed on the path's tail: mount it under any prefix. */
70
+ fetch(req: Request): Promise<Response>;
71
+ GET(req: Request): Promise<Response>;
72
+ POST(req: Request): Promise<Response>;
73
+ }
74
+ export declare function createRightsHandlers(o: RightsHandlersOptions): RightsHandlers;
75
+ export {};