@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/dist/index.d.ts CHANGED
@@ -20,7 +20,7 @@ import type { ConformanceReport } from './executor-conformance.js';
20
20
  * This package's version, sent in the default User-Agent. Cloudflare answers a request with
21
21
  * no recognisable User-Agent with `403 error code: 1010`; a test pins this to package.json.
22
22
  */
23
- export declare const SDK_VERSION = "0.9.1";
23
+ export declare const SDK_VERSION = "0.11.0";
24
24
  export interface CookieMunchOptions {
25
25
  apiKey: string;
26
26
  /** "https://api.cookiemunch.net" — the /v1 prefix is appended automatically. */
@@ -1167,6 +1167,24 @@ export interface ApiKeyPrefix {
1167
1167
  /** The dated API version this key is pinned to. */
1168
1168
  apiVersion?: string;
1169
1169
  }
1170
+ /** A signed audit-anchor receipt (POST /v1/anchors). Verify `signature` (Ed25519, base64url) over the canonical JSON of every other field with the key `keyId` in /v1/partner/jwks.json. */
1171
+ export interface AnchorReceipt {
1172
+ id: string;
1173
+ org: string;
1174
+ stream: string;
1175
+ head: string;
1176
+ seq: number;
1177
+ receivedAt: string;
1178
+ keyId: string;
1179
+ alg: 'EdDSA';
1180
+ signature: string;
1181
+ }
1182
+ /** GET /v1/anchors/{stream}: newest first; pass `nextBefore` as `before` until it is null. */
1183
+ export interface AnchorList {
1184
+ stream: string;
1185
+ anchors: AnchorReceipt[];
1186
+ nextBefore: number | null;
1187
+ }
1170
1188
  /** A live organisation's sandbox (GET /v1/sandbox). */
1171
1189
  export interface SandboxOrg {
1172
1190
  orgId: string;
@@ -1308,8 +1326,12 @@ export interface WebhookSubscription {
1308
1326
  id: string;
1309
1327
  orgId: string;
1310
1328
  url: string;
1329
+ /** Shown once on create, and only when Cookie Munch generated it. */
1311
1330
  secret?: string;
1331
+ /** The patterns as given: event names, `*`, `<family>.*`. */
1312
1332
  events: string[];
1333
+ /** The events those patterns take today. A wildcard also takes events added later. */
1334
+ matches?: string[];
1313
1335
  cbid: string | null;
1314
1336
  active: boolean;
1315
1337
  createdAt: number;
@@ -1719,8 +1741,11 @@ export interface DsarExportResult {
1719
1741
  /** Input for creating a webhook subscription. */
1720
1742
  export interface WebhookCreate {
1721
1743
  url: string;
1744
+ /** Event names, `*` (every event; needs a full-access key) or `<family>.*` such as `consent.*`. */
1722
1745
  events: string[];
1723
1746
  cbid?: string;
1747
+ /** Your own signing secret, 32–256 printable characters. Not echoed back; omit it and one is generated and returned once. */
1748
+ secret?: string;
1724
1749
  }
1725
1750
  /** Fields to change on a webhook subscription. Only those present are sent. */
1726
1751
  export interface WebhookUpdate {
@@ -2154,6 +2179,8 @@ export interface DsarExecutor {
2154
2179
  lastSignedCompletionAt?: number | null;
2155
2180
  /** Where signed cm-standard-v2 completions go. */
2156
2181
  completionsUrl?: string;
2182
+ /** True: its erasures wait (status `awaiting_approval`) for `approveTask` or `rejectTask`. */
2183
+ requireApproval?: boolean;
2157
2184
  /** On create only: the completion secret, shown this once. */
2158
2185
  completionSecret?: string;
2159
2186
  }
@@ -2180,6 +2207,8 @@ export interface ExecutorHealth {
2180
2207
  code?: string;
2181
2208
  /** When a check last passed. */
2182
2209
  lastSuccessAt?: number;
2210
+ /** The `X-Version` the system last answered with. */
2211
+ version?: string;
2183
2212
  /** On reads: true when the last check is more than an hour old, with the command that re-checks it in `hint`. */
2184
2213
  stale?: boolean;
2185
2214
  hint?: string;
@@ -2216,6 +2245,8 @@ export interface DsarExecutorPresetInput {
2216
2245
  label?: string;
2217
2246
  /** Default 2. */
2218
2247
  orchestrator?: 1 | 2;
2248
+ /** Park its erasures for a person's approval. Default false. */
2249
+ requireApproval?: boolean;
2219
2250
  }
2220
2251
  /** What `upgradeExecutorPreset` takes: a version, a preset to adopt, or params to add. */
2221
2252
  export interface ExecutorPresetUpgrade {
@@ -2334,8 +2365,17 @@ export interface DsarExecutorInput {
2334
2365
  profile: ExecutorProfile;
2335
2366
  /** Default 2. */
2336
2367
  orchestrator?: 1 | 2;
2368
+ /** Park its erasures for a person's approval. Default false. */
2369
+ requireApproval?: boolean;
2337
2370
  }
2338
- export type DsarExecutorCreated = DsarExecutor;
2371
+ export type DsarExecutorCreated = DsarExecutor & {
2372
+ warnings?: Array<{
2373
+ field: string;
2374
+ reason: string;
2375
+ }>;
2376
+ /** With `connectExecutor(…, { verify })`: what the canary probe found. */
2377
+ verify?: ExecutorTestResult;
2378
+ };
2339
2379
  /** The fields `updateExecutor` may change. */
2340
2380
  export interface DsarExecutorPatch {
2341
2381
  profile?: unknown;
@@ -2351,24 +2391,28 @@ export interface DsarExecutorPatch {
2351
2391
  presetPinned?: boolean;
2352
2392
  /** Opt in to (2) or out of (1) orchestrator v2. */
2353
2393
  orchestrator?: 1 | 2;
2394
+ /** Park its erasures for approval; false stops parking new ones (parked ones still wait). */
2395
+ requireApproval?: boolean;
2354
2396
  }
2355
2397
  /** One entry in a fulfilment task's status history. */
2356
2398
  export interface DsarTaskEvent {
2357
2399
  at: number;
2358
2400
  kind: 'created' | 'changed' | 'version_changed';
2359
- status: 'pending' | 'running' | 'done' | 'failed' | 'cancelled';
2401
+ status: DsarTaskStatus;
2360
2402
  detail?: string;
2361
2403
  outcome?: DsarTaskOutcome;
2362
2404
  scheduledFor?: number;
2363
2405
  externalRef?: string;
2364
2406
  }
2407
+ /** `awaiting_approval`: an erasure parked until `approveTask` (→ pending) or `rejectTask` (→ failed). */
2408
+ export type DsarTaskStatus = 'pending' | 'running' | 'done' | 'failed' | 'cancelled' | 'awaiting_approval';
2365
2409
  /** What a task's answer means for the person. `exported` only once the export was fetched. */
2366
2410
  export type DsarTaskOutcome = 'located' | 'exported' | 'erased' | 'no_record' | 'done_without_artifact' | 'not_applicable' | 'already_erased' | 'external_identity';
2367
2411
  /** One request made to a connected system on a task's behalf, credentials removed. Kept 30 days. */
2368
2412
  export interface DsarTaskLogEntry {
2369
2413
  id?: string;
2370
2414
  system?: string;
2371
- step: 'lookup' | 'open' | 'poll' | 'export' | 'grant';
2415
+ step: 'lookup' | 'open' | 'poll' | 'export' | 'grant' | 'cancel' | 'preview';
2372
2416
  method: string;
2373
2417
  path: string;
2374
2418
  /** Absent when the system never answered. */
@@ -2395,7 +2439,7 @@ export interface DsarTask {
2395
2439
  requestId: string;
2396
2440
  system: string;
2397
2441
  operation: FulfillmentOp;
2398
- status: 'pending' | 'running' | 'done' | 'failed' | 'cancelled';
2442
+ status: DsarTaskStatus;
2399
2443
  detail?: string;
2400
2444
  outcome?: DsarTaskOutcome;
2401
2445
  attempts: number;
@@ -2415,6 +2459,19 @@ export interface DsarTask {
2415
2459
  upstreamVersions?: string[];
2416
2460
  /** The system was redeployed while this task was in flight. */
2417
2461
  versionChanged: boolean;
2462
+ /** For an erasure awaiting approval: what the system said it would remove (≤4000 chars, redacted). */
2463
+ preview?: {
2464
+ at: number;
2465
+ text?: string;
2466
+ error?: string;
2467
+ };
2468
+ /** The account at that system, when one email matched several there (one task each). */
2469
+ subjectKey?: string;
2470
+ /** How the requester was verified, as sent to the system. */
2471
+ verification?: {
2472
+ method: 'code' | 'session' | 'account_link' | 'attested' | 'none' | 'controller';
2473
+ emailVerified: boolean;
2474
+ };
2418
2475
  }
2419
2476
  /** What testing a connection found. Nothing was filed. */
2420
2477
  export interface ExecutorTestResult {
@@ -2443,6 +2500,8 @@ export interface ExecutorTestResult {
2443
2500
  }>;
2444
2501
  /** Present when the test used the org canary address, with the address in `email`. */
2445
2502
  canary?: boolean;
2503
+ /** The accounts the lookup found, when one email matched several (subjectLookup.idsPath). */
2504
+ subjects?: string[];
2446
2505
  email?: string;
2447
2506
  /** Every upstream answer, as the system gave it (credentials removed). */
2448
2507
  exchanges: DsarTaskLogEntry[];
@@ -2740,7 +2799,7 @@ export interface CookieMunchClient {
2740
2799
  dryRun: true;
2741
2800
  }): Promise<DeclarationDryRun>;
2742
2801
  /**
2743
- * Declare many at once (all-or-nothing). `replace` also removes declarations not listed;
2802
+ * Declare many at once (all-or-nothing). `replace` also removes your own declarations not listed (vendor-preset rows stay);
2744
2803
  * `dryRun` answers with what would be stored and a diff, storing nothing.
2745
2804
  */
2746
2805
  declareCookies(cbid: string, cookies: Array<DeclaredCookieInput & {
@@ -2827,8 +2886,17 @@ export interface CookieMunchClient {
2827
2886
  * wrapped as `{ request }`, so `create(...).request.id` is what you pass here.
2828
2887
  */
2829
2888
  get(id: string): Promise<DsarRequest>;
2889
+ /** `skipped`: connections scoped to another property (or to one, when the request names none), never planned for it. */
2830
2890
  create(input: DsarCreate & ResourceTagsInput): Promise<{
2831
2891
  request: DsarRequest;
2892
+ consent?: {
2893
+ count: number;
2894
+ sites: string[];
2895
+ };
2896
+ skipped?: Array<{
2897
+ system: string;
2898
+ reason: string;
2899
+ }>;
2832
2900
  }>;
2833
2901
  /** `opts.messageToSubject` replaces the line the subject reads in the notice — for a rejection, why. */
2834
2902
  advance(id: string, toStatus: DsarStatus, opts?: {
@@ -2864,6 +2932,8 @@ export interface CookieMunchClient {
2864
2932
  }>;
2865
2933
  /** Requests carrying one X-Trace-Id (`GET /v1/dsar?trace=`). */
2866
2934
  listByTrace(trace: string): Promise<DsarRequest[]>;
2935
+ /** Requests with one origin (`GET /v1/dsar?origin=`): `account_deletion`, those a person's own account deletion opened. */
2936
+ listByOrigin(origin: 'account_deletion'): Promise<DsarRequest[]>;
2867
2937
  /**
2868
2938
  * Open a case fanned out to every connected system (POST /v1/dsar/requests). `opts.idempotencyKey`
2869
2939
  * (16–128 chars) makes a retry answer the same case; `opts.trace` passes your X-Trace-Id on.
@@ -3133,7 +3203,14 @@ export interface CookieMunchClient {
3133
3203
  * Connect one. The secret is stored encrypted and never returned; the response carries
3134
3204
  * the URL to point that system's webhook at.
3135
3205
  */
3136
- connectExecutor(input: (DsarExecutorInput | DsarExecutorPresetInput) & ResourceTagsInput): Promise<DsarExecutorCreated>;
3206
+ connectExecutor(input: (DsarExecutorInput | DsarExecutorPresetInput) & ResourceTagsInput,
3207
+ /**
3208
+ * `verify: true` probes it first (a canary locate; no erasure is sent) and refuses a failing one
3209
+ * (422 `verify_failed`, nothing stored); `'warn'` stores it with the verdict in `warnings`.
3210
+ */
3211
+ opts?: {
3212
+ verify?: true | 'warn';
3213
+ }): Promise<DsarExecutorCreated>;
3137
3214
  /**
3138
3215
  * Change a connection in place. Only what you send changes, and the stored credential
3139
3216
  * is kept unless you pass a new `secretKey` — so adding a field to a profile does not
@@ -3178,6 +3255,12 @@ export interface CookieMunchClient {
3178
3255
  * its behalf (`log`, redacted, 30 days) and whether that system's version changed mid-way.
3179
3256
  */
3180
3257
  task(requestId: string, taskId: string): Promise<DsarTask>;
3258
+ /** Approve an erasure awaiting approval: it is sent now. Audited with your key. 409 when it is not waiting. */
3259
+ approveTask(requestId: string, taskId: string): Promise<DsarTask>;
3260
+ /** Reject an erasure awaiting approval: nothing is sent and the task fails with `reason`. Audited. */
3261
+ rejectTask(requestId: string, taskId: string, opts?: {
3262
+ reason?: string;
3263
+ }): Promise<DsarTask>;
3181
3264
  /**
3182
3265
  * Try a connection against one email without filing anything: runs the lookup, returns
3183
3266
  * the raw (redacted) upstream answers and what a real request would have sent.
@@ -3280,6 +3363,26 @@ export interface CookieMunchClient {
3280
3363
  versions(): Promise<ApiVersions>;
3281
3364
  /** What the org may use: every plan feature (`whiteLabel`, `tcf`, …) and limit, in the suite-standard shape. Any key may read it. */
3282
3365
  entitlements(): Promise<Entitlements>;
3366
+ /**
3367
+ * Audit anchoring: post the head of your own hash-chained log and get a receipt signed by Cookie Munch's
3368
+ * platform key. `seq` must strictly increase per stream; the same seq and head again returns the original
3369
+ * receipt, a different head for an anchored seq is a 409 (`anchor_head_mismatch`). Needs `anchors:write` / `anchors:read`.
3370
+ */
3371
+ anchors: {
3372
+ /** Anchor `head` at `seq` on `stream` (POST /v1/anchors). */
3373
+ create(input: {
3374
+ stream: string;
3375
+ head: string;
3376
+ seq: number;
3377
+ }): Promise<AnchorReceipt>;
3378
+ /** A stream's receipts, newest first (GET /v1/anchors/{stream}). */
3379
+ list(stream: string, query?: {
3380
+ limit?: number;
3381
+ before?: number;
3382
+ }): Promise<AnchorList>;
3383
+ /** One receipt (GET /v1/anchors/{stream}/{id}). */
3384
+ get(stream: string, id: string): Promise<AnchorReceipt>;
3385
+ };
3283
3386
  /**
3284
3387
  * The organisation's sandbox: a separate test organisation with the same people and plan features, never billed.
3285
3388
  * Its keys are test-format keys and its rights requests are all tests. `create` and `delete` need an unscoped key or `orgs:create`.
@@ -3426,9 +3529,12 @@ export interface CookieMunchClient {
3426
3529
  /** Change or pause a subscription. `{ active: false }` pauses without deleting it. */
3427
3530
  update(id: string, patch: WebhookUpdate): Promise<WebhookSubscription>;
3428
3531
  delete(id: string): Promise<void>;
3429
- /** Rotate the signing secret. The new secret is returned once. */
3430
- rollSecret(id: string): Promise<{
3431
- secret: string;
3532
+ /** Rotate the signing secret. A generated one is returned once; `{ secret }` you choose is not echoed (`rolled: true`). */
3533
+ rollSecret(id: string, opts?: {
3534
+ secret?: string;
3535
+ }): Promise<{
3536
+ secret?: string;
3537
+ rolled?: boolean;
3432
3538
  }>;
3433
3539
  /** Send a signed test event now and report what the endpoint answered. */
3434
3540
  test(id: string): Promise<WebhookTestResult>;
package/dist/index.js CHANGED
@@ -9,6 +9,12 @@ import {
9
9
  verifyDecisionSignature,
10
10
  verifyToken
11
11
  } from "./chunk-OSVTQPQG.js";
12
+ import {
13
+ CANARY_ISSUER,
14
+ CONFORMANCE_RULES,
15
+ canaryIdentityFor,
16
+ runExecutorConformance
17
+ } from "./chunk-U5OJSPE5.js";
12
18
 
13
19
  // src/webhooks.ts
14
20
  import { createHmac, timingSafeEqual } from "crypto";
@@ -45,181 +51,6 @@ function mintConsentSyncToken(input) {
45
51
  return `cms1.${payload}.${sig}`;
46
52
  }
47
53
 
48
- // src/executor-conformance.ts
49
- var CANARY_ISSUER = "https://canary.cookiemunch.net";
50
- var CANARY_EMAIL_RE = /^canary-([0-9a-f]{16})@canary\.cookiemunch\.net$/;
51
- function canaryIdentityFor(canaryEmail) {
52
- const m = CANARY_EMAIL_RE.exec(canaryEmail.trim().toLowerCase());
53
- if (!m) throw new Error("canaryEmail must be the canary address canary-<16 hex>@canary.cookiemunch.net");
54
- return { iss: CANARY_ISSUER, sub: `canary_${m[1]}`, ext: `canary:${m[1]}` };
55
- }
56
- var CONFORMANCE_RULES = [
57
- { id: "auth.required", title: "A request without the credential is refused (401 or 403)." },
58
- { id: "lookup.email.found", title: "The canary subject is found by its email." },
59
- { id: "lookup.identity.found", title: "The canary subject is found by its identity alone (iss + sub), and it is the same subject." },
60
- { id: "lookup.unknown.no_match", title: 'An address nobody has answers { subjectId: null, status: "no_match" }.' },
61
- { id: "lookup.identity_beats_email", title: "An identity that matches no one is never matched through the email of someone else." },
62
- { id: "requests.bad_body.400", title: "A request with an unknown type answers 400 { error, field }." },
63
- { id: "requests.alias.canonical_type", title: 'An alias (erasure) is accepted and answered with canonicalType "deletion".' },
64
- { 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.' },
65
- { id: "requests.idempotency.replay", title: "The same Idempotency-Key with the same body returns the same request, not a second one." },
66
- { id: "requests.idempotency.mismatch_409", title: "The same Idempotency-Key with a different body answers 409." },
67
- { id: "requests.get", title: "GET /privacy/requests/{id} returns the request, still scheduled." },
68
- { id: "requests.unknown.404", title: "An unknown request id answers 404." },
69
- { id: "requests.cancel.before_execute_after", title: 'A cancel before executeAfter answers status "cancelled".' },
70
- { id: "requests.cancel.keeps_data", title: "After a cancel the subject is still there: a lookup finds it." },
71
- { id: "version.header", title: "Answers carry X-Version (the highest contract version the executor supports)." }
72
- ];
73
- function randomTag() {
74
- const bytes = new Uint8Array(8);
75
- const c = globalThis.crypto;
76
- if (c?.getRandomValues) c.getRandomValues(bytes);
77
- else for (let i = 0; i < bytes.length; i++) bytes[i] = Math.floor(Math.random() * 256);
78
- return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
79
- }
80
- function field(json, key) {
81
- return json && typeof json === "object" ? json[key] : void 0;
82
- }
83
- async function runExecutorConformance(o) {
84
- const doFetch = o.fetch ?? fetch;
85
- const now = o.now ?? (() => Date.now());
86
- const base = o.baseUrl.replace(/\/+$/, "") + (o.prefix ?? "/privacy");
87
- let url;
88
- try {
89
- url = new URL(base);
90
- } catch {
91
- throw new Error("baseUrl must be an absolute URL");
92
- }
93
- if (url.protocol !== "https:" && !(url.protocol === "http:" && /^(localhost|127\.0\.0\.1|\[::1\])$/.test(url.hostname))) {
94
- throw new Error("baseUrl must be https (http only for localhost)");
95
- }
96
- const identity = canaryIdentityFor(o.canaryEmail);
97
- const canaryEmail = o.canaryEmail.trim().toLowerCase();
98
- const auth = o.auth ?? { header: "Authorization", format: "Bearer {secret}" };
99
- const run = o.runId ?? randomTag();
100
- const results = /* @__PURE__ */ new Map();
101
- let version;
102
- const set = (id, status, detail) => {
103
- const rule = CONFORMANCE_RULES.find((r) => r.id === id);
104
- results.set(id, { id, title: rule.title, status, ...detail ? { detail } : {} });
105
- };
106
- const pass = (id) => set(id, "pass");
107
- const fail = (id, detail) => set(id, "fail", detail);
108
- const skip = (id, detail) => set(id, "skip", detail);
109
- async function call(method, path, body, extra = {}, withAuth = true) {
110
- const headers = {
111
- accept: "application/json",
112
- "X-Version": "2",
113
- ...withAuth ? { [auth.header]: auth.format.replace("{secret}", o.secret) } : {},
114
- ...body !== void 0 ? { "content-type": "application/json" } : {},
115
- ...extra
116
- };
117
- try {
118
- const res = await doFetch(base + path, { method, headers, ...body !== void 0 ? { body: JSON.stringify(body) } : {} });
119
- const text = await res.text().catch(() => "");
120
- let json;
121
- try {
122
- json = text ? JSON.parse(text) : void 0;
123
- } catch {
124
- json = void 0;
125
- }
126
- const v = res.headers.get("x-version");
127
- if (v && !version) version = v;
128
- return { status: res.status, json, version: v };
129
- } catch (err) {
130
- return { status: 0, json: void 0, version: null, error: err instanceof Error ? err.message : String(err) };
131
- }
132
- }
133
- const said = (a) => a.status === 0 ? `unreachable: ${a.error}` : `HTTP ${a.status} ${JSON.stringify(a.json ?? null).slice(0, 200)}`;
134
- const key = (rule) => `conformance:${run}:${rule}`;
135
- const anon = await call("POST", "/lookup", { emails: [canaryEmail] }, {}, false);
136
- if (anon.status === 0) {
137
- for (const r of CONFORMANCE_RULES) skip(r.id, said(anon));
138
- fail("auth.required", said(anon));
139
- return report();
140
- }
141
- if (anon.status === 401 || anon.status === 403) pass("auth.required");
142
- else fail("auth.required", `without a credential: ${said(anon)}`);
143
- const byEmail = await call("POST", "/lookup", { emails: [canaryEmail] }, { "Idempotency-Key": key("lookup-email") });
144
- const subjectId = field(byEmail.json, "subjectId");
145
- if (byEmail.status === 200 && field(byEmail.json, "status") === "found" && typeof subjectId === "string" && subjectId) pass("lookup.email.found");
146
- else fail("lookup.email.found", `the canary address: ${said(byEmail)}`);
147
- const byIdentity = await call("POST", "/lookup", { emails: [], identity, atlas: identity }, { "Idempotency-Key": key("lookup-identity") });
148
- if (byIdentity.status === 200 && field(byIdentity.json, "status") === "found" && field(byIdentity.json, "subjectId") === subjectId && typeof subjectId === "string") {
149
- pass("lookup.identity.found");
150
- } else {
151
- fail("lookup.identity.found", `the canary identity ${identity.sub}: ${said(byIdentity)}${typeof subjectId === "string" ? ` (by email it was ${subjectId})` : ""}`);
152
- }
153
- const nobody = await call("POST", "/lookup", { emails: [`nobody-${run}@canary.cookiemunch.net`] }, { "Idempotency-Key": key("lookup-nobody") });
154
- if (nobody.status === 200 && field(nobody.json, "status") === "no_match" && field(nobody.json, "subjectId") === null) pass("lookup.unknown.no_match");
155
- else fail("lookup.unknown.no_match", said(nobody));
156
- const stranger = { iss: identity.iss, sub: `canary_other_${run}` };
157
- const crossed = await call("POST", "/lookup", { emails: [canaryEmail], identity: stranger, atlas: stranger }, { "Idempotency-Key": key("lookup-crossed") });
158
- if (crossed.status === 200 && field(crossed.json, "status") === "no_match") pass("lookup.identity_beats_email");
159
- else {
160
- fail(
161
- "lookup.identity_beats_email",
162
- `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`
163
- );
164
- }
165
- const bad = await call("POST", "/requests", { subjectId: subjectId ?? "x", type: "teleport", caseId: `conformance-${run}` }, { "Idempotency-Key": key("bad-body") });
166
- if (bad.status === 400 && typeof field(bad.json, "error") === "string" && typeof field(bad.json, "field") === "string") pass("requests.bad_body.400");
167
- else fail("requests.bad_body.400", said(bad));
168
- if (typeof subjectId !== "string") {
169
- 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"]) {
170
- skip(id, "needs the canary subject, which the lookup did not find");
171
- }
172
- } else {
173
- const executeAfter = new Date(Math.ceil((now() + 36e5) / 1e3) * 1e3).toISOString();
174
- const body = { subjectId, type: "erasure", executeAfter, caseId: `conformance-${run}`, reason: "Cookie Munch conformance check (canary subject)" };
175
- const opened = await call("POST", "/requests", body, { "Idempotency-Key": key("open") });
176
- const id = field(opened.json, "id");
177
- const st = field(opened.json, "status");
178
- if ((opened.status === 200 || opened.status === 201) && field(opened.json, "canonicalType") === "deletion") pass("requests.alias.canonical_type");
179
- else fail("requests.alias.canonical_type", `type "erasure": ${said(opened)}`);
180
- const eta = field(opened.json, "eta");
181
- if ((opened.status === 200 || opened.status === 201) && st === "scheduled" && typeof eta === "string" && Date.parse(eta) === Date.parse(executeAfter)) {
182
- pass("requests.execute_after.scheduled");
183
- } else if (st === "completed" || st === "completed_with_retention" || st === "processing") {
184
- fail("requests.execute_after.scheduled", `executeAfter is ${executeAfter}, an hour away, and the executor answered "${String(st)}": it acted before executeAfter`);
185
- } else {
186
- fail("requests.execute_after.scheduled", `expected status "scheduled" with eta ${executeAfter}: ${said(opened)}`);
187
- }
188
- const replay = await call("POST", "/requests", body, { "Idempotency-Key": key("open") });
189
- if (typeof id === "string" && (replay.status === 200 || replay.status === 201) && field(replay.json, "id") === id) pass("requests.idempotency.replay");
190
- else fail("requests.idempotency.replay", `a retry with the same key: ${said(replay)}${typeof id === "string" ? ` (first answer id ${id})` : ""}`);
191
- const mismatch = await call("POST", "/requests", { ...body, reason: "a different body" }, { "Idempotency-Key": key("open") });
192
- if (mismatch.status === 409) pass("requests.idempotency.mismatch_409");
193
- else fail("requests.idempotency.mismatch_409", `the same key with a different body: ${said(mismatch)}`);
194
- if (typeof id !== "string") {
195
- skip("requests.get", "no request id came back");
196
- skip("requests.cancel.before_execute_after", "no request id came back");
197
- skip("requests.cancel.keeps_data", "no request id came back");
198
- } else {
199
- const got = await call("GET", `/requests/${encodeURIComponent(id)}`, void 0, { "Idempotency-Key": key("get") });
200
- if (got.status === 200 && field(got.json, "id") === id && field(got.json, "status") === "scheduled") pass("requests.get");
201
- else fail("requests.get", said(got));
202
- const cancelled = await call("POST", `/requests/${encodeURIComponent(id)}/cancel`, {}, { "Idempotency-Key": key("cancel") });
203
- if (cancelled.status === 200 && field(cancelled.json, "status") === "cancelled") pass("requests.cancel.before_execute_after");
204
- else fail("requests.cancel.before_execute_after", said(cancelled));
205
- const after = await call("POST", "/lookup", { emails: [canaryEmail] }, { "Idempotency-Key": key("lookup-after-cancel") });
206
- if (after.status === 200 && field(after.json, "status") === "found") pass("requests.cancel.keeps_data");
207
- else fail("requests.cancel.keeps_data", `a lookup after the cancel: ${said(after)} \u2014 a cancel before executeAfter must leave the data intact`);
208
- }
209
- }
210
- const unknown = await call("GET", `/requests/conformance-missing-${run}`, void 0, { "Idempotency-Key": key("unknown") });
211
- if (unknown.status === 404) pass("requests.unknown.404");
212
- else fail("requests.unknown.404", said(unknown));
213
- if (version) pass("version.header");
214
- else fail("version.header", "no answer carried an X-Version header");
215
- return report();
216
- function report() {
217
- const rules = CONFORMANCE_RULES.map((r) => results.get(r.id) ?? { id: r.id, title: r.title, status: "skip", detail: "not reached" });
218
- const count = (s) => rules.filter((r) => r.status === s).length;
219
- return { ok: count("fail") === 0, passed: count("pass"), failed: count("fail"), skipped: count("skip"), rules, ...version ? { version } : {} };
220
- }
221
- }
222
-
223
54
  // src/token-exchange.ts
224
55
  var TokenExchangeError = class extends Error {
225
56
  constructor(error, status, description) {
@@ -257,7 +88,7 @@ async function exchangeOperatorToken(input) {
257
88
  }
258
89
 
259
90
  // src/index.ts
260
- var SDK_VERSION = "0.9.1";
91
+ var SDK_VERSION = "0.11.0";
261
92
  var CookieMunchApiError = class extends Error {
262
93
  constructor(status, message, code, body) {
263
94
  super(message);
@@ -477,6 +308,7 @@ function createCookieMunch(opts) {
477
308
  },
478
309
  markTest: (id) => request("POST", `/dsar/${enc(id)}/mark-test`),
479
310
  listByTrace: (trace) => get(`/dsar${qs({ trace })}`),
311
+ listByOrigin: (origin) => get(`/dsar${qs({ origin })}`),
480
312
  openCase: (input, o) => request("POST", "/dsar/requests", input, false, {
481
313
  ...o?.idempotencyKey ? { "Idempotency-Key": o.idempotencyKey } : {},
482
314
  ...o?.trace ? { "X-Trace-Id": o.trace } : {}
@@ -590,7 +422,7 @@ function createCookieMunch(opts) {
590
422
  reportTask: (taskId, ok, error) => request("POST", `/dsar/agent/tasks/${enc(taskId)}/result`, { ok, ...error !== void 0 ? { error } : {} }),
591
423
  executors: (o) => get(`/dsar/executors${metaQs(o)}`),
592
424
  executor: (id) => get(`/dsar/executors/${enc(id)}`),
593
- connectExecutor: (input) => request("POST", "/dsar/executors", input),
425
+ connectExecutor: (input, o) => request("POST", `/dsar/executors${qs({ verify: o?.verify ? o.verify === "warn" ? "warn" : "1" : void 0 })}`, input),
594
426
  updateExecutor: (id, patch) => request("PATCH", `/dsar/executors/${enc(id)}`, patch),
595
427
  dryRunConnectExecutor: (input) => request("POST", "/dsar/executors?dryRun=1", input),
596
428
  executorPresets: () => get("/dsar/executors/presets"),
@@ -604,6 +436,8 @@ function createCookieMunch(opts) {
604
436
  },
605
437
  taskExport: (requestId, taskId) => get(`/dsar/${enc(requestId)}/tasks/${enc(taskId)}/export`),
606
438
  task: (requestId, taskId) => get(`/dsar/${enc(requestId)}/tasks/${enc(taskId)}`),
439
+ approveTask: (requestId, taskId) => request("POST", `/dsar/${enc(requestId)}/tasks/${enc(taskId)}/approve`),
440
+ rejectTask: (requestId, taskId, o = {}) => request("POST", `/dsar/${enc(requestId)}/tasks/${enc(taskId)}/reject`, o.reason !== void 0 ? { reason: o.reason } : {}),
607
441
  testExecutor: (id, input) => request("POST", `/dsar/executors/${enc(id)}/test`, input),
608
442
  recheckExecutor: (id) => request("POST", `/dsar/executors/${enc(id)}/recheck`),
609
443
  executorHealthHistory: (id, opts2 = {}) => get(`/dsar/executors/${enc(id)}/health/history${qs({ limit: opts2.limit })}`),
@@ -643,6 +477,11 @@ function createCookieMunch(opts) {
643
477
  usage: () => get("/usage"),
644
478
  entitlements: () => get("/entitlements"),
645
479
  versions: () => get("/versions"),
480
+ anchors: {
481
+ create: (input) => request("POST", "/anchors", input),
482
+ list: (stream, query) => get(`/anchors/${enc(stream)}${qs(query)}`),
483
+ get: (stream, id) => get(`/anchors/${enc(stream)}/${enc(id)}`)
484
+ },
646
485
  sandbox: {
647
486
  get: async () => (await get("/sandbox")).sandbox,
648
487
  create: (input) => request("POST", "/sandbox", input ?? {}),
@@ -723,7 +562,7 @@ function createCookieMunch(opts) {
723
562
  delete: async (id) => {
724
563
  await request("DELETE", `/webhooks/${enc(id)}`);
725
564
  },
726
- rollSecret: (id) => request("POST", `/webhooks/${enc(id)}/roll`),
565
+ rollSecret: (id, opts2) => request("POST", `/webhooks/${enc(id)}/roll`, opts2?.secret ? { secret: opts2.secret } : void 0),
727
566
  test: (id) => request("POST", `/webhooks/${enc(id)}/test`),
728
567
  deadLetters: () => get("/webhooks/dead-letters"),
729
568
  replayDeadLetter: (id) => request("POST", `/webhooks/dead-letters/${enc(id)}/replay`),