@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 +16 -0
- package/README.md +18 -0
- package/dist/chunk-U5OJSPE5.js +182 -0
- package/dist/chunk-U5OJSPE5.js.map +1 -0
- package/dist/index.cjs +13 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +116 -10
- package/dist/index.js +17 -178
- package/dist/index.js.map +1 -1
- package/dist/rights.cjs +258 -0
- package/dist/rights.cjs.map +1 -0
- package/dist/rights.d.ts +75 -0
- package/dist/rights.js +232 -0
- package/dist/rights.js.map +1 -0
- package/dist/types.d.ts +16 -0
- package/package.json +6 -1
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.
|
|
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:
|
|
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:
|
|
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
|
|
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.
|
|
3430
|
-
rollSecret(id: string
|
|
3431
|
-
secret
|
|
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.
|
|
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",
|
|
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`),
|