@cookiemunch/cli 0.4.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/LICENSE +21 -0
- package/dist/bin.js +2 -2
- package/dist/bin.js.map +1 -1
- package/dist/chunk-LJ433UVC.js +1785 -0
- package/dist/chunk-LJ433UVC.js.map +1 -0
- package/dist/index.d.ts +74 -5
- package/dist/index.js +15 -1
- package/package.json +2 -2
- package/dist/chunk-43RTEWFN.js +0 -873
- package/dist/chunk-43RTEWFN.js.map +0 -1
|
@@ -0,0 +1,1785 @@
|
|
|
1
|
+
// src/diff.ts
|
|
2
|
+
var READ_ONLY_CONFIG_FIELDS = ["effective", "warnings", "designWarnings", "region", "regulation", "copy", "rightsFormUrl", "version", "observeToken", "sdkBoot", "originEnv"];
|
|
3
|
+
function withoutReadOnly(cfg) {
|
|
4
|
+
const out2 = { ...cfg };
|
|
5
|
+
for (const k of READ_ONLY_CONFIG_FIELDS) delete out2[k];
|
|
6
|
+
return out2;
|
|
7
|
+
}
|
|
8
|
+
function isPlainObject(v) {
|
|
9
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
10
|
+
}
|
|
11
|
+
function mergeLikeServer(base, patch) {
|
|
12
|
+
const out2 = { ...base };
|
|
13
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
14
|
+
const b = base[k];
|
|
15
|
+
if (v === null) delete out2[k];
|
|
16
|
+
else out2[k] = isPlainObject(v) ? mergeLikeServer(isPlainObject(b) ? b : {}, v) : v;
|
|
17
|
+
}
|
|
18
|
+
return out2;
|
|
19
|
+
}
|
|
20
|
+
function join(path, key) {
|
|
21
|
+
return path ? `${path}.${key}` : key;
|
|
22
|
+
}
|
|
23
|
+
function diffJson(before, after, path = "") {
|
|
24
|
+
if (isPlainObject(before) && isPlainObject(after)) {
|
|
25
|
+
const keys = [.../* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])].sort();
|
|
26
|
+
return keys.flatMap((k) => {
|
|
27
|
+
const p = join(path, k);
|
|
28
|
+
if (!(k in after)) return [{ path: p, kind: "removed", from: before[k] }];
|
|
29
|
+
if (!(k in before)) return [{ path: p, kind: "added", to: after[k] }];
|
|
30
|
+
return diffJson(before[k], after[k], p);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
if (Array.isArray(before) && Array.isArray(after)) {
|
|
34
|
+
const out2 = [];
|
|
35
|
+
const n = Math.max(before.length, after.length);
|
|
36
|
+
for (let i = 0; i < n; i++) {
|
|
37
|
+
const p = `${path}[${i}]`;
|
|
38
|
+
if (i >= after.length) out2.push({ path: p, kind: "removed", from: before[i] });
|
|
39
|
+
else if (i >= before.length) out2.push({ path: p, kind: "added", to: after[i] });
|
|
40
|
+
else out2.push(...diffJson(before[i], after[i], p));
|
|
41
|
+
}
|
|
42
|
+
return out2;
|
|
43
|
+
}
|
|
44
|
+
if (JSON.stringify(before) === JSON.stringify(after)) return [];
|
|
45
|
+
return [{ path: path || "(root)", kind: "changed", from: before, to: after }];
|
|
46
|
+
}
|
|
47
|
+
function show(v) {
|
|
48
|
+
const s = JSON.stringify(v);
|
|
49
|
+
return s === void 0 ? "undefined" : s.length > 120 ? `${s.slice(0, 117)}...` : s;
|
|
50
|
+
}
|
|
51
|
+
function formatChanges(changes) {
|
|
52
|
+
return changes.map(
|
|
53
|
+
(c) => c.kind === "added" ? `+ ${c.path}: ${show(c.to)}` : c.kind === "removed" ? `- ${c.path}: ${show(c.from)}` : `~ ${c.path}: ${show(c.from)} -> ${show(c.to)}`
|
|
54
|
+
).join("\n");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// src/rights-page.ts
|
|
58
|
+
import { CookieMunchApiError } from "@cookiemunch/sdk";
|
|
59
|
+
var isConflict = (e) => e instanceof CookieMunchApiError && e.status === 409;
|
|
60
|
+
function baseFrom(flag, read, h) {
|
|
61
|
+
if (flag === void 0) return read ?? null;
|
|
62
|
+
if (typeof flag === "string" && /^\d+$/.test(flag)) return Number(flag);
|
|
63
|
+
if (typeof flag === "string" && /^\d{4}-\d{2}-\d{2}T/.test(flag) && Number.isFinite(Date.parse(flag))) return flag;
|
|
64
|
+
throw h.usage("--base must be the draft\u2019s updatedAt (epoch ms, as pull prints it, or an ISO date)");
|
|
65
|
+
}
|
|
66
|
+
function issuesOf(e) {
|
|
67
|
+
if (!(e instanceof CookieMunchApiError) || e.status !== 422) return null;
|
|
68
|
+
const body = e.body;
|
|
69
|
+
return body?.issues ?? body?.problems ?? [];
|
|
70
|
+
}
|
|
71
|
+
async function rightsPageCommand(sub, p, io, client, h) {
|
|
72
|
+
const cbid = h.requireSite();
|
|
73
|
+
const report = (list) => io.stderr(`${list.map((i) => ` ${i.path || "(document)"}: ${i.message}`).join("\n")}
|
|
74
|
+
`);
|
|
75
|
+
if (sub === "pull") {
|
|
76
|
+
const { draft, updatedAt } = await client.rightsPage.get(cbid);
|
|
77
|
+
const text = `${JSON.stringify(draft, null, 2)}
|
|
78
|
+
`;
|
|
79
|
+
const out2 = typeof p.flags.out === "string" ? p.flags.out : void 0;
|
|
80
|
+
if (out2) {
|
|
81
|
+
await io.writeFile(out2, text);
|
|
82
|
+
io.stderr(`Wrote ${cbid}'s rights page draft to ${out2}
|
|
83
|
+
`);
|
|
84
|
+
} else io.stdout(text);
|
|
85
|
+
if (updatedAt != null) io.stderr(`To refuse a push if anyone saves the draft after this pull, push with: --base ${updatedAt}
|
|
86
|
+
`);
|
|
87
|
+
return 0;
|
|
88
|
+
}
|
|
89
|
+
if (sub === "diff" || sub === "push") {
|
|
90
|
+
const file = await h.readJson(p.positional[2]);
|
|
91
|
+
const read = await client.rightsPage.get(cbid);
|
|
92
|
+
const current = read.draft;
|
|
93
|
+
const base = baseFrom(p.flags.base, read.updatedAt, h);
|
|
94
|
+
let proposed2;
|
|
95
|
+
try {
|
|
96
|
+
proposed2 = (await client.rightsPage.dryRun(cbid, file, { mode: "put" })).draft;
|
|
97
|
+
} catch (e) {
|
|
98
|
+
const list = issuesOf(e);
|
|
99
|
+
if (!list) throw e;
|
|
100
|
+
io.stderr(`${p.positional[2]} is not a valid rights page:
|
|
101
|
+
`);
|
|
102
|
+
report(list);
|
|
103
|
+
return 1;
|
|
104
|
+
}
|
|
105
|
+
const changes = diffJson(current, proposed2);
|
|
106
|
+
if (p.flags.json === true && (sub === "diff" || !changes.length)) io.stdout(`${JSON.stringify({ cbid, changes }, null, 2)}
|
|
107
|
+
`);
|
|
108
|
+
if (!changes.length) {
|
|
109
|
+
io.stderr(`No changes: ${cbid}'s rights page draft already matches.
|
|
110
|
+
`);
|
|
111
|
+
return 0;
|
|
112
|
+
}
|
|
113
|
+
if (p.flags.json !== true) io.stdout(`${formatChanges(changes)}
|
|
114
|
+
`);
|
|
115
|
+
if (sub === "diff") {
|
|
116
|
+
io.stderr(`${changes.length} change${changes.length === 1 ? "" : "s"} (nothing saved). Apply with: cookiemunch rights-page push --site ${cbid} ${p.positional[2]}
|
|
117
|
+
`);
|
|
118
|
+
return p.flags["exit-code"] === true ? 1 : 0;
|
|
119
|
+
}
|
|
120
|
+
let saved;
|
|
121
|
+
try {
|
|
122
|
+
saved = await client.rightsPage.put(cbid, file, { baseUpdatedAt: base });
|
|
123
|
+
} catch (e) {
|
|
124
|
+
if (!isConflict(e)) throw e;
|
|
125
|
+
io.stderr(`Not saved: ${cbid}'s rights page draft changed since ${p.flags.base !== void 0 ? "the --base you gave" : "this diff"} (someone else saved it).
|
|
126
|
+
See what changed with: cookiemunch rights-page diff --site ${cbid} ${p.positional[2]}
|
|
127
|
+
then push again.
|
|
128
|
+
`);
|
|
129
|
+
return 1;
|
|
130
|
+
}
|
|
131
|
+
io.stderr(`Saved the draft (${changes.length} change${changes.length === 1 ? "" : "s"}). Nothing is live until: cookiemunch rights-page publish --site ${cbid}
|
|
132
|
+
`);
|
|
133
|
+
if (saved.warnings.length) {
|
|
134
|
+
io.stderr("It cannot be published yet:\n");
|
|
135
|
+
report(saved.warnings);
|
|
136
|
+
}
|
|
137
|
+
return 0;
|
|
138
|
+
}
|
|
139
|
+
if (sub === "publish") {
|
|
140
|
+
const base = p.flags.base !== void 0 ? baseFrom(p.flags.base, null, h) : (await client.rightsPage.get(cbid)).updatedAt ?? null;
|
|
141
|
+
if (p.flags.yes !== true) {
|
|
142
|
+
if (!io.confirm) throw h.usage("rights-page publish puts the draft live and there is no terminal to confirm in. Pass --yes to publish without a prompt.");
|
|
143
|
+
if (!await io.confirm(`Publish ${cbid}'s rights page draft? It goes live at once. [y/N] `)) {
|
|
144
|
+
io.stderr("Not published.\n");
|
|
145
|
+
return 1;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
const r = await client.rightsPage.publish(cbid, { baseUpdatedAt: base });
|
|
150
|
+
io.stderr(`Published version ${r.version} of ${cbid}'s rights page.
|
|
151
|
+
`);
|
|
152
|
+
return 0;
|
|
153
|
+
} catch (e) {
|
|
154
|
+
if (isConflict(e)) {
|
|
155
|
+
io.stderr(`Nothing was published: ${cbid}'s rights page draft changed since it was read (someone else saved it).
|
|
156
|
+
Review it with: cookiemunch rights-page pull --site ${cbid}
|
|
157
|
+
then publish again.
|
|
158
|
+
`);
|
|
159
|
+
return 1;
|
|
160
|
+
}
|
|
161
|
+
const list = issuesOf(e);
|
|
162
|
+
if (!list) throw e;
|
|
163
|
+
io.stderr("The rights page cannot be published:\n");
|
|
164
|
+
report(list);
|
|
165
|
+
return 1;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
throw h.usage(`unknown rights-page command: ${sub ?? "(none)"} \u2014 use pull, diff, push or publish`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// src/csp.ts
|
|
172
|
+
var KNOWN_MANIFEST_HOSTS = {
|
|
173
|
+
cookiemunch: ["cookiemunch.net", "api.cookiemunch.net"],
|
|
174
|
+
atlas: ["atlasauth.net", "api.atlasauth.net"],
|
|
175
|
+
chatly: ["chatlychat.com", "api.chatlychat.com"],
|
|
176
|
+
forumulate: ["forumulate.com"],
|
|
177
|
+
referalli: ["referalli.com"]
|
|
178
|
+
};
|
|
179
|
+
var ALIASES = { referally: "referalli" };
|
|
180
|
+
var CSP_FRAMEWORKS = ["next", "express", "nginx"];
|
|
181
|
+
function manifestSources(list, opts) {
|
|
182
|
+
const sources = [];
|
|
183
|
+
const unknown = [];
|
|
184
|
+
for (const raw of list.split(",").map((s) => s.trim()).filter(Boolean)) {
|
|
185
|
+
if (/^https:\/\//i.test(raw)) {
|
|
186
|
+
sources.push({ label: raw, urls: [raw] });
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
const name = ALIASES[raw.toLowerCase()] ?? raw.toLowerCase();
|
|
190
|
+
const hosts = KNOWN_MANIFEST_HOSTS[name];
|
|
191
|
+
if (!hosts) {
|
|
192
|
+
unknown.push(raw);
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (name === "cookiemunch" && opts.site) {
|
|
196
|
+
sources.push({ label: `cookiemunch (site ${opts.site})`, urls: [`${opts.apiBase.replace(/\/$/, "")}/v1/embed?cbid=${encodeURIComponent(opts.site)}`], product: name });
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
sources.push({ label: name, urls: hosts.map((h) => `https://${h}/.well-known/forgedeck-embed.json`), product: name });
|
|
200
|
+
}
|
|
201
|
+
return { sources, unknown };
|
|
202
|
+
}
|
|
203
|
+
var ORIGIN = /^(https|wss):\/\/[^\s*'"]+$/;
|
|
204
|
+
var KEYWORD = /^('unsafe-inline'|'unsafe-eval'|'wasm-unsafe-eval'|'unsafe-hashes'|data:|blob:)$/;
|
|
205
|
+
function mergeManifests(manifests) {
|
|
206
|
+
const byDirective = /* @__PURE__ */ new Map();
|
|
207
|
+
const add = (directive, list, allowKeywords) => {
|
|
208
|
+
if (!Array.isArray(list) || !/^[a-z-]+-src$|^form-action$/.test(directive)) return;
|
|
209
|
+
let set = byDirective.get(directive);
|
|
210
|
+
if (!set) byDirective.set(directive, set = /* @__PURE__ */ new Set());
|
|
211
|
+
for (const src of list) {
|
|
212
|
+
if (typeof src !== "string") continue;
|
|
213
|
+
if (src.includes("{")) continue;
|
|
214
|
+
if (ORIGIN.test(src) || allowKeywords && KEYWORD.test(src)) set.add(src);
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
for (const m of manifests) {
|
|
218
|
+
for (const s of Object.values(m.surfaces ?? {})) {
|
|
219
|
+
for (const [d, list] of Object.entries(s.csp ?? {})) add(d, list, false);
|
|
220
|
+
for (const [d, list] of Object.entries(s.requires ?? {})) add(d, list, true);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
const out2 = /* @__PURE__ */ new Map();
|
|
224
|
+
out2.set("default-src", ["'self'"]);
|
|
225
|
+
for (const [d, set] of [...byDirective.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
226
|
+
if (set.size === 0) continue;
|
|
227
|
+
const keywords = [...set].filter((s) => !ORIGIN.test(s)).sort();
|
|
228
|
+
const origins = [...set].filter((s) => ORIGIN.test(s)).sort();
|
|
229
|
+
out2.set(d, ["'self'", ...keywords, ...origins]);
|
|
230
|
+
}
|
|
231
|
+
return out2;
|
|
232
|
+
}
|
|
233
|
+
function placeholders(manifests) {
|
|
234
|
+
const out2 = /* @__PURE__ */ new Set();
|
|
235
|
+
for (const m of manifests) {
|
|
236
|
+
for (const s of Object.values(m.surfaces ?? {})) {
|
|
237
|
+
for (const map of [s.csp, s.requires]) {
|
|
238
|
+
for (const list of Object.values(map ?? {})) {
|
|
239
|
+
if (Array.isArray(list)) {
|
|
240
|
+
for (const src of list) if (typeof src === "string" && src.includes("{")) out2.add(src);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return [...out2].sort();
|
|
247
|
+
}
|
|
248
|
+
function policyString(policy) {
|
|
249
|
+
return [...policy.entries()].map(([d, list]) => `${d} ${list.join(" ")}`).join("; ");
|
|
250
|
+
}
|
|
251
|
+
function renderPolicy(policy, framework) {
|
|
252
|
+
const value = policyString(policy);
|
|
253
|
+
if (framework === "next") {
|
|
254
|
+
return [
|
|
255
|
+
"// next.config.js",
|
|
256
|
+
"async headers() {",
|
|
257
|
+
" return [{",
|
|
258
|
+
" source: '/(.*)',",
|
|
259
|
+
` headers: [{ key: 'Content-Security-Policy', value: ${JSON.stringify(value)} }],`,
|
|
260
|
+
" }];",
|
|
261
|
+
"},"
|
|
262
|
+
].join("\n");
|
|
263
|
+
}
|
|
264
|
+
if (framework === "express") {
|
|
265
|
+
return `app.use((req, res, next) => { res.setHeader('Content-Security-Policy', ${JSON.stringify(value)}); next(); });`;
|
|
266
|
+
}
|
|
267
|
+
if (framework === "nginx") return `add_header Content-Security-Policy "${value.replace(/"/g, '\\"')}" always;`;
|
|
268
|
+
return value;
|
|
269
|
+
}
|
|
270
|
+
async function fetchManifest(src, fetchImpl) {
|
|
271
|
+
const tried = [];
|
|
272
|
+
for (const url of src.urls) {
|
|
273
|
+
try {
|
|
274
|
+
const res = await fetchImpl(url, { headers: { Accept: "application/json" }, redirect: "manual" });
|
|
275
|
+
if (!res.ok) {
|
|
276
|
+
tried.push(`${url} answered ${res.status}`);
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
const doc = await res.json();
|
|
280
|
+
if (!doc || typeof doc !== "object" || !doc.surfaces || typeof doc.surfaces !== "object") {
|
|
281
|
+
tried.push(`${url} is not an embed manifest (no surfaces)`);
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
if (src.product && doc.product !== src.product) {
|
|
285
|
+
tried.push(`${url} speaks for "${String(doc.product)}", not ${src.product}`);
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
return { ok: true, manifest: doc, url };
|
|
289
|
+
} catch (e) {
|
|
290
|
+
tried.push(`${url}: ${e.message}`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return { ok: false, error: tried.join("; ") || "no URL to try" };
|
|
294
|
+
}
|
|
295
|
+
async function cspCommand(opts, io, usage) {
|
|
296
|
+
if (!opts.merge) throw usage("--merge is required: vendor names (cookiemunch, atlas, chatly, forumulate, referalli) or manifest URLs, comma-separated");
|
|
297
|
+
if (opts.framework !== void 0 && !CSP_FRAMEWORKS.includes(opts.framework)) {
|
|
298
|
+
throw usage(`--framework must be one of ${CSP_FRAMEWORKS.join(", ")}`);
|
|
299
|
+
}
|
|
300
|
+
const { sources, unknown } = manifestSources(opts.merge, { apiBase: opts.apiBase, ...opts.site ? { site: opts.site } : {} });
|
|
301
|
+
if (unknown.length) throw usage(`unknown vendor: ${unknown.join(", ")}. Name one of ${Object.keys(KNOWN_MANIFEST_HOSTS).join(", ")}, or pass its manifest URL (https://\u2026/.well-known/forgedeck-embed.json).`);
|
|
302
|
+
const manifests = [];
|
|
303
|
+
let failed = 0;
|
|
304
|
+
for (const src of sources) {
|
|
305
|
+
const r = await fetchManifest(src, io.fetch);
|
|
306
|
+
if (r.ok) manifests.push(r.manifest);
|
|
307
|
+
else {
|
|
308
|
+
failed++;
|
|
309
|
+
io.stderr(`${src.label}: no manifest \u2014 ${r.error}
|
|
310
|
+
`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
if (manifests.length === 0) return 1;
|
|
314
|
+
const policy = mergeManifests(manifests);
|
|
315
|
+
io.stdout(`${renderPolicy(policy, opts.framework)}
|
|
316
|
+
`);
|
|
317
|
+
const left = placeholders(manifests);
|
|
318
|
+
if (left.length) io.stderr(`Tenant-specific sources left out (fill in your own value): ${left.join(", ")}
|
|
319
|
+
`);
|
|
320
|
+
return failed ? 1 : 0;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// src/cli.ts
|
|
324
|
+
import { createCookieMunch, CookieMunchApiError as CookieMunchApiError2, mintConsentSyncToken, runExecutorConformance } from "@cookiemunch/sdk";
|
|
325
|
+
|
|
326
|
+
// src/integrations.ts
|
|
327
|
+
var out = (io, v) => io.stdout(`${JSON.stringify(v, null, 2)}
|
|
328
|
+
`);
|
|
329
|
+
async function siteCommand(sub, io, client, h, file) {
|
|
330
|
+
if (sub === "brand") return brandCommand(io, client, h, file);
|
|
331
|
+
if (sub !== "update") throw h.usage("usage: cookiemunch site update --site <cbid> [--domain <host>] [--name <name>] [--aliases a,b] [--dry-run]");
|
|
332
|
+
const cbid = h.requireSite();
|
|
333
|
+
const domain = h.flag("domain");
|
|
334
|
+
const name = h.flag("name");
|
|
335
|
+
const aliases = h.flag("aliases");
|
|
336
|
+
const patch = {};
|
|
337
|
+
if (typeof domain === "string") patch.domain = domain;
|
|
338
|
+
if (h.flag("clear-name") === true) patch.name = null;
|
|
339
|
+
else if (typeof name === "string") patch.name = name;
|
|
340
|
+
if (typeof aliases === "string") patch.aliases = aliases.split(",").map((a) => a.trim()).filter(Boolean);
|
|
341
|
+
if (Object.keys(patch).length === 0) throw h.usage("nothing to change: give --domain, --name, --clear-name or --aliases");
|
|
342
|
+
const r = await client.sites.update(cbid, patch, { dryRun: h.flag("dry-run") === true });
|
|
343
|
+
out(io, r);
|
|
344
|
+
if (r.effects.verification === "reset") io.stderr("The domain moved: verify the new domain (GET /v1/sites/{cbid}/verify/challenge).\n");
|
|
345
|
+
for (const w of r.warnings ?? []) io.stderr(`warning (${w.field}): ${w.reason}
|
|
346
|
+
`);
|
|
347
|
+
return 0;
|
|
348
|
+
}
|
|
349
|
+
async function brandCommand(io, client, h, file) {
|
|
350
|
+
const cbid = h.requireSite();
|
|
351
|
+
const url = h.flag("url");
|
|
352
|
+
const r = typeof url === "string" ? await client.sites.setBrand(cbid, { url }, { dryRun: h.flag("dry-run") === true }) : file ? await client.sites.setBrand(cbid, await h.readJson(file), { dryRun: h.flag("dry-run") === true }) : await client.sites.getBrand(cbid);
|
|
353
|
+
out(io, r);
|
|
354
|
+
for (const w of r.warnings) io.stderr(`warning (${w.role}): ${w.message}
|
|
355
|
+
`);
|
|
356
|
+
for (const w of r.configWarnings ?? []) io.stderr(`warning (${w.field}): ${w.reason}
|
|
357
|
+
`);
|
|
358
|
+
return 0;
|
|
359
|
+
}
|
|
360
|
+
async function healthCommand(io, client, h) {
|
|
361
|
+
const failOn = h.flag("fail-on") ?? "warning";
|
|
362
|
+
if (failOn !== "error" && failOn !== "warning" && failOn !== "never") throw h.usage("--fail-on must be error, warning or never");
|
|
363
|
+
const r = await client.integrationHealth(h.site());
|
|
364
|
+
if (h.flag("json") === true) out(io, r);
|
|
365
|
+
else {
|
|
366
|
+
for (const c of r.checks) {
|
|
367
|
+
const mark = c.waived ? "WAIV" : c.ok === true ? "ok " : c.ok === false ? c.severity === "error" ? "FAIL" : "WARN" : "-- ";
|
|
368
|
+
const net = c.hop ? ` (${c.hop}${c.status !== void 0 ? ` ${c.status}` : ""}${c.elapsedMs !== void 0 ? `, ${c.elapsedMs} ms` : ""})` : "";
|
|
369
|
+
io.stdout(`${mark} ${c.check} \u2014 ${c.subject}${c.detail ? `: ${c.detail}` : ""}${net}${c.waived ? ` \u2014 waived: ${c.waived.reason}` : ""}
|
|
370
|
+
`);
|
|
371
|
+
const fix = c.fix ?? c.fixHint;
|
|
372
|
+
if (c.ok === false && fix) io.stdout(` fix: ${fix}
|
|
373
|
+
`);
|
|
374
|
+
}
|
|
375
|
+
io.stdout(`${r.counts.error} error(s), ${r.counts.warning} warning(s), ${r.counts.notChecked} not checked${r.counts.waived ? `, ${r.counts.waived} waived` : ""}
|
|
376
|
+
`);
|
|
377
|
+
}
|
|
378
|
+
if (failOn === "never") return 0;
|
|
379
|
+
const failing = r.checks.filter((c) => c.ok === false && (c.severity === "error" || failOn === "warning" && c.severity === "warning"));
|
|
380
|
+
return failing.length ? 1 : 0;
|
|
381
|
+
}
|
|
382
|
+
async function provisionCommand(file, io, client, h) {
|
|
383
|
+
const input = await h.readJson(file);
|
|
384
|
+
const v2 = input;
|
|
385
|
+
if (v2.forgedeck !== void 0 || v2.site && typeof v2.site === "object") {
|
|
386
|
+
const body = { ...v2 };
|
|
387
|
+
if (body.self) {
|
|
388
|
+
const { keyEnv, ...self } = body.self;
|
|
389
|
+
const secret = typeof keyEnv === "string" ? io.env[keyEnv] : void 0;
|
|
390
|
+
if (!secret) throw h.usage(`self needs its key: set self.keyEnv in the file and export that variable`);
|
|
391
|
+
body.self = { ...self, secret };
|
|
392
|
+
}
|
|
393
|
+
if (body.atlas && io.env.COOKIEMUNCH_ATLAS_SECRET_KEY) body.atlas = { ...body.atlas, secretKey: io.env.COOKIEMUNCH_ATLAS_SECRET_KEY };
|
|
394
|
+
if (body.forumulate && io.env.COOKIEMUNCH_FORUMULATE_TOKEN) body.forumulate = { ...body.forumulate, token: io.env.COOKIEMUNCH_FORUMULATE_TOKEN };
|
|
395
|
+
const fromStep = h.flag("from-step");
|
|
396
|
+
const r2 = await client.provision(body, { dryRun: h.flag("dry-run") === true, ...typeof fromStep === "string" ? { fromStep } : {} });
|
|
397
|
+
if (h.flag("json") === true) out(io, r2);
|
|
398
|
+
else {
|
|
399
|
+
for (const s of r2.steps) {
|
|
400
|
+
const w = s.waitingOn;
|
|
401
|
+
const waiting = typeof w === "string" ? w : w ? `${w.platform}: ${(w.items ?? []).join(", ")}` : "";
|
|
402
|
+
io.stdout(`${s.status.padEnd(12)} ${s.id ?? s.step}${s.detail ? ` \u2014 ${s.detail}` : ""}${waiting ? ` (waiting on ${waiting})` : ""}
|
|
403
|
+
`);
|
|
404
|
+
}
|
|
405
|
+
io.stdout(`${r2.dryRun ? "dry run: " : ""}${r2.summary.changed} changed, ${r2.summary.unchanged} unchanged, ${r2.summary.waiting} waiting, ${r2.summary.failed} failed
|
|
406
|
+
`);
|
|
407
|
+
}
|
|
408
|
+
return r2.summary.failed ? 1 : 0;
|
|
409
|
+
}
|
|
410
|
+
const withSecrets = { ...input };
|
|
411
|
+
if (input.atlas || io.env.COOKIEMUNCH_ATLAS_SECRET_KEY) {
|
|
412
|
+
const secretKey = io.env.COOKIEMUNCH_ATLAS_SECRET_KEY ?? input.atlas?.secretKey;
|
|
413
|
+
if (!secretKey) throw h.usage("set COOKIEMUNCH_ATLAS_SECRET_KEY for the atlas step");
|
|
414
|
+
withSecrets.atlas = { ...input.atlas ?? {}, secretKey };
|
|
415
|
+
}
|
|
416
|
+
if (input.forumulate) {
|
|
417
|
+
const token = io.env.COOKIEMUNCH_FORUMULATE_TOKEN ?? input.forumulate.token;
|
|
418
|
+
if (!token) throw h.usage("set COOKIEMUNCH_FORUMULATE_TOKEN for the forumulate step");
|
|
419
|
+
withSecrets.forumulate = { ...input.forumulate, token };
|
|
420
|
+
}
|
|
421
|
+
const r = await client.provision(withSecrets, { dryRun: h.flag("dry-run") === true });
|
|
422
|
+
if (h.flag("json") === true) {
|
|
423
|
+
out(io, r);
|
|
424
|
+
return r.summary.failed ? 1 : 0;
|
|
425
|
+
}
|
|
426
|
+
for (const s of r.steps) {
|
|
427
|
+
io.stdout(`${s.status.padEnd(12)} ${s.step}${s.detail ? ` \u2014 ${s.detail}` : ""}${s.waitingOn ? ` (waiting on ${s.waitingOn})` : ""}
|
|
428
|
+
`);
|
|
429
|
+
}
|
|
430
|
+
io.stdout(`${r.dryRun ? "dry run: " : ""}${r.summary.changed} changed, ${r.summary.unchanged} unchanged, ${r.summary.waiting} waiting, ${r.summary.failed} failed
|
|
431
|
+
`);
|
|
432
|
+
return r.summary.failed ? 1 : 0;
|
|
433
|
+
}
|
|
434
|
+
async function partnersCommand(sub, rest, io, client, h) {
|
|
435
|
+
const cbid = h.requireSite();
|
|
436
|
+
if (sub === "list") {
|
|
437
|
+
out(io, await client.sites.listPartners(cbid));
|
|
438
|
+
return 0;
|
|
439
|
+
}
|
|
440
|
+
if (sub === "link") {
|
|
441
|
+
const partner = h.flag("partner");
|
|
442
|
+
const externalId = h.flag("external-id");
|
|
443
|
+
if (typeof partner !== "string" || !/^[a-z][a-z0-9_]{1,39}$/.test(partner)) throw h.usage("--partner <id> is required: forumulate, atlas, chatly, referalli, or one registered on the deployment");
|
|
444
|
+
if (typeof externalId !== "string") throw h.usage("--external-id <the partner\u2019s account id> is required");
|
|
445
|
+
const label = h.flag("label");
|
|
446
|
+
const input = { partner, externalId, ...typeof label === "string" ? { label } : {} };
|
|
447
|
+
out(io, await client.sites.linkPartner(cbid, input, { dryRun: h.flag("dry-run") === true }));
|
|
448
|
+
return 0;
|
|
449
|
+
}
|
|
450
|
+
if (sub === "unlink") {
|
|
451
|
+
const id = rest[0];
|
|
452
|
+
if (!id) throw h.usage("usage: cookiemunch partners unlink <linkId> --site <cbid>");
|
|
453
|
+
out(io, await client.sites.unlinkPartner(cbid, id));
|
|
454
|
+
return 0;
|
|
455
|
+
}
|
|
456
|
+
throw h.usage("usage: cookiemunch partners list|link|unlink --site <cbid>");
|
|
457
|
+
}
|
|
458
|
+
async function keysCommand(sub, io, client, h) {
|
|
459
|
+
if (sub !== "report") throw h.usage("usage: cookiemunch keys report [--unused-days <n>] [--expiring-days <n>] [--json]");
|
|
460
|
+
const n = (name) => {
|
|
461
|
+
const v = h.flag(name);
|
|
462
|
+
if (v === void 0) return void 0;
|
|
463
|
+
const x = Number(v);
|
|
464
|
+
if (!Number.isInteger(x) || x < 1 || x > 3650) throw h.usage(`--${name} must be whole days from 1 to 3650`);
|
|
465
|
+
return x;
|
|
466
|
+
};
|
|
467
|
+
const unusedDays = n("unused-days");
|
|
468
|
+
const expiringDays = n("expiring-days");
|
|
469
|
+
const r = await client.keys.report({ ...unusedDays ? { unusedDays } : {}, ...expiringDays ? { expiringDays } : {} });
|
|
470
|
+
if (h.flag("json") === true) {
|
|
471
|
+
out(io, r);
|
|
472
|
+
return 0;
|
|
473
|
+
}
|
|
474
|
+
const section = (title, rows) => {
|
|
475
|
+
io.stdout(`${title}: ${rows.length}
|
|
476
|
+
`);
|
|
477
|
+
for (const k of rows) io.stdout(` ${k.prefix}${k.name ? ` (${k.name})` : ""} \u2014 ${k.reason}
|
|
478
|
+
`);
|
|
479
|
+
};
|
|
480
|
+
section(`Unused for ${r.unusedDays}+ days`, r.unused);
|
|
481
|
+
section("Broad access", r.broad);
|
|
482
|
+
section(`Expiring within ${r.expiringDays} days`, r.expiringSoon);
|
|
483
|
+
const revoked = r.revoked ?? [];
|
|
484
|
+
io.stdout(`Revoked: ${revoked.length}
|
|
485
|
+
`);
|
|
486
|
+
for (const k of revoked) io.stdout(` ${k.prefix}${k.name ? ` (${k.name})` : ""} \u2014 ${k.reason} ${new Date(k.revokedAt).toISOString()} by ${k.revokedBy}; last used ${k.lastUsedAt ? new Date(k.lastUsedAt).toISOString() : "never"}
|
|
487
|
+
`);
|
|
488
|
+
io.stdout(`${r.total} key(s), ${r.noExpiry} with no expiry
|
|
489
|
+
`);
|
|
490
|
+
return 0;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// src/design-warnings.ts
|
|
494
|
+
function where(f) {
|
|
495
|
+
const w = f.where ?? {};
|
|
496
|
+
return [
|
|
497
|
+
f.layer === "banner" ? "first layer" : `${f.layer} layer`,
|
|
498
|
+
w.view ? `view "${w.view}"` : "",
|
|
499
|
+
w.variant && w.variant !== "base" ? `variant ${w.variant}` : "",
|
|
500
|
+
w.theme === "dark" ? "dark theme" : "",
|
|
501
|
+
w.breakpoint === "mobile" ? "mobile" : "",
|
|
502
|
+
w.language ? `language ${w.language}` : ""
|
|
503
|
+
].filter(Boolean).join(", ");
|
|
504
|
+
}
|
|
505
|
+
function formatDesignWarnings(findings) {
|
|
506
|
+
return findings.map((f) => `${f.severity === "warning" ? "warning" : "notice "} ${f.code} ${where(f)}
|
|
507
|
+
${f.detail}
|
|
508
|
+
fix: ${f.fix}`).join("\n");
|
|
509
|
+
}
|
|
510
|
+
function countBySeverity(findings) {
|
|
511
|
+
const warnings = findings.filter((f) => f.severity === "warning").length;
|
|
512
|
+
return { warnings, notices: findings.length - warnings };
|
|
513
|
+
}
|
|
514
|
+
function summaryLine(findings) {
|
|
515
|
+
const { warnings, notices } = countBySeverity(findings);
|
|
516
|
+
if (warnings + notices === 0) return "No design warnings: Accept all and Reject all have equal weight.";
|
|
517
|
+
if (warnings === 0) {
|
|
518
|
+
return `Design: 0 warnings, ${notices} notice${notices === 1 ? "" : "s"} \u2014 worth a look, not a finding that Accept is favoured. See https://cookiemunch.net/docs/fair-consent-buttons`;
|
|
519
|
+
}
|
|
520
|
+
return `Design: ${warnings} warning${warnings === 1 ? "" : "s"}, ${notices} notice${notices === 1 ? "" : "s"} \u2014 Accept is pushed harder than Reject. Regulators treat that as a dark pattern; make both buttons the same style. See https://cookiemunch.net/docs/fair-consent-buttons`;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// src/ops.ts
|
|
524
|
+
var json = (h) => h.flag("json") === true;
|
|
525
|
+
var print = (io, v) => io.stdout(`${JSON.stringify(v, null, 2)}
|
|
526
|
+
`);
|
|
527
|
+
var str = (h, name) => {
|
|
528
|
+
const v = h.flag(name);
|
|
529
|
+
return typeof v === "string" ? v : void 0;
|
|
530
|
+
};
|
|
531
|
+
async function versionCommand(io, cliVersion, base, asJson) {
|
|
532
|
+
if (!asJson) {
|
|
533
|
+
io.stdout(`${cliVersion}
|
|
534
|
+
`);
|
|
535
|
+
return 0;
|
|
536
|
+
}
|
|
537
|
+
let api = null;
|
|
538
|
+
try {
|
|
539
|
+
const res = await io.fetch(`${base.replace(/\/+$/, "")}/v1/openapi.json`);
|
|
540
|
+
if (res.ok) api = (await res.json()).info?.version ?? null;
|
|
541
|
+
} catch {
|
|
542
|
+
}
|
|
543
|
+
print(io, { cli: cliVersion, api });
|
|
544
|
+
return 0;
|
|
545
|
+
}
|
|
546
|
+
function cookieRows(file) {
|
|
547
|
+
const rows = Array.isArray(file) ? file : Array.isArray(file.cookies) ? file.cookies : null;
|
|
548
|
+
if (!rows || !rows.every((r) => r && typeof r === "object" && typeof r.name === "string")) {
|
|
549
|
+
throw new Error('the file must hold { "cookies": [ { "name", "category", "purpose", \u2026 } ] } or that array');
|
|
550
|
+
}
|
|
551
|
+
return rows;
|
|
552
|
+
}
|
|
553
|
+
async function cookiesFileCommand(sub, io, client, h) {
|
|
554
|
+
const cbid = h.requireSite();
|
|
555
|
+
const path = h.positional[2];
|
|
556
|
+
if (!path) throw h.usage(`usage: cookiemunch cookies ${sub} <file> --site <cbid> [--replace]`);
|
|
557
|
+
let rows;
|
|
558
|
+
try {
|
|
559
|
+
rows = cookieRows(await h.readJson(path));
|
|
560
|
+
} catch (e) {
|
|
561
|
+
throw h.usage(e.message);
|
|
562
|
+
}
|
|
563
|
+
const replace = h.flag("replace") === true;
|
|
564
|
+
const dry = await client.sites.declareCookies(cbid, rows, { replace, dryRun: true });
|
|
565
|
+
const diff = dry.diff ?? { added: [], removed: [], changed: [] };
|
|
566
|
+
const n = diff.added.length + diff.removed.length + diff.changed.length;
|
|
567
|
+
if (json(h)) print(io, { cbid, changes: n, diff, ...dry.warnings?.length ? { warnings: dry.warnings } : {} });
|
|
568
|
+
else if (n === 0) io.stderr(`No changes: ${cbid}'s declarations already match.
|
|
569
|
+
`);
|
|
570
|
+
else {
|
|
571
|
+
for (const c of diff.added) io.stdout(`+ ${c.name} (${c.category})
|
|
572
|
+
`);
|
|
573
|
+
for (const c of diff.removed) io.stdout(`- ${c.name}
|
|
574
|
+
`);
|
|
575
|
+
for (const c of diff.changed) io.stdout(`~ ${c.name}: ${formatChanges(diffJson(c.before, c.after))}
|
|
576
|
+
`);
|
|
577
|
+
}
|
|
578
|
+
for (const w of dry.warnings ?? []) io.stderr(`warning: ${JSON.stringify(w)}
|
|
579
|
+
`);
|
|
580
|
+
if (sub === "diff") return n > 0 && h.flag("exit-code") === true ? 1 : 0;
|
|
581
|
+
if (n === 0) return 0;
|
|
582
|
+
const saved = await client.sites.declareCookies(cbid, rows, { replace });
|
|
583
|
+
io.stderr(`Applied: ${saved.saved} saved, ${saved.removed} removed.
|
|
584
|
+
`);
|
|
585
|
+
return 0;
|
|
586
|
+
}
|
|
587
|
+
var EXECUTOR_FIELDS = ["system", "baseUrl", "profile", "auto", "subjectNote", "cbid", "preset", "presetVersion", "presetParams"];
|
|
588
|
+
function executorFileView(e) {
|
|
589
|
+
const out2 = { id: e.id };
|
|
590
|
+
for (const k of EXECUTOR_FIELDS) {
|
|
591
|
+
const v = e[k];
|
|
592
|
+
if (v !== void 0) out2[k] = v;
|
|
593
|
+
}
|
|
594
|
+
return out2;
|
|
595
|
+
}
|
|
596
|
+
async function executorsFileCommand(sub, io, client, h) {
|
|
597
|
+
const current = (await client.fulfillment.executors()).map(executorFileView);
|
|
598
|
+
if (sub === "pull") {
|
|
599
|
+
const text = `${JSON.stringify({ executors: current }, null, 2)}
|
|
600
|
+
`;
|
|
601
|
+
const path2 = str(h, "out") ?? h.positional[2];
|
|
602
|
+
if (path2) {
|
|
603
|
+
await io.writeFile(path2, text);
|
|
604
|
+
io.stderr(`Wrote ${current.length} connection(s) to ${path2} (no secrets).
|
|
605
|
+
`);
|
|
606
|
+
} else io.stdout(text);
|
|
607
|
+
return 0;
|
|
608
|
+
}
|
|
609
|
+
const path = h.positional[2];
|
|
610
|
+
if (!path) throw h.usage(`usage: cookiemunch executors ${sub} <file> [--secret-env NAME]`);
|
|
611
|
+
const file = await h.readJson(path);
|
|
612
|
+
const wanted = Array.isArray(file.executors) ? file.executors : null;
|
|
613
|
+
if (!wanted) throw h.usage(`${path} must hold { "executors": [ \u2026 ] } (cookiemunch executors pull writes one)`);
|
|
614
|
+
for (const w of wanted) {
|
|
615
|
+
for (const k of ["secretKey", "secret", "webhookSecret"]) if (k in w) throw h.usage(`${path} carries ${k}: secrets never go in this file; pass --secret-env NAME`);
|
|
616
|
+
}
|
|
617
|
+
const plan = [];
|
|
618
|
+
for (const w of wanted) {
|
|
619
|
+
const match = current.find((c) => w.id && c.id === w.id || !w.id && c.system === w.system);
|
|
620
|
+
const { id: _i, ...want } = w;
|
|
621
|
+
if (!match) {
|
|
622
|
+
plan.push({ system: String(w.system), action: "create", changes: diffJson({}, want) });
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
625
|
+
const { id, ...have } = match;
|
|
626
|
+
const changes = diffJson(Object.fromEntries(Object.keys(want).map((k) => [k, have[k] ?? null])), want);
|
|
627
|
+
plan.push({ system: String(w.system), action: changes.length ? "update" : "unchanged", id: String(id), changes });
|
|
628
|
+
}
|
|
629
|
+
const pending = plan.filter((p) => p.action !== "unchanged");
|
|
630
|
+
if (json(h)) print(io, { plan });
|
|
631
|
+
else if (!pending.length) io.stderr("No changes: every connection already matches.\n");
|
|
632
|
+
else for (const p of pending) io.stdout(`${p.action} ${p.system}${p.id ? ` (${p.id})` : ""}
|
|
633
|
+
${formatChanges(p.changes).replace(/^/gm, " ")}
|
|
634
|
+
`);
|
|
635
|
+
if (sub === "diff") return pending.length && h.flag("exit-code") === true ? 1 : 0;
|
|
636
|
+
const secretEnv = str(h, "secret-env");
|
|
637
|
+
const creating = pending.filter((p) => p.action === "create");
|
|
638
|
+
if (creating.length && !(secretEnv && io.env[secretEnv])) {
|
|
639
|
+
throw h.usage(`creating ${creating.map((p) => p.system).join(", ")} needs its API key: pass --secret-env NAME and export NAME`);
|
|
640
|
+
}
|
|
641
|
+
for (const p of pending) {
|
|
642
|
+
const w = wanted.find((x) => String(x.system) === p.system);
|
|
643
|
+
const { id: _i, ...body } = w;
|
|
644
|
+
if (p.action === "update") {
|
|
645
|
+
await client.fulfillment.updateExecutor(p.id, Object.fromEntries(p.changes.map((c) => c.path.split(/[.[]/)[0]).filter((k, i, a) => a.indexOf(k) === i).map((k) => [k, body[k]])));
|
|
646
|
+
} else {
|
|
647
|
+
const secretKey = secretEnv ? io.env[secretEnv] : void 0;
|
|
648
|
+
if (!secretKey) throw h.usage(`creating ${p.system} needs its API key: set --secret-env NAME and export NAME`);
|
|
649
|
+
await client.fulfillment.connectExecutor({ ...body, secretKey });
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
io.stderr(`Applied ${pending.length} change(s).
|
|
653
|
+
`);
|
|
654
|
+
return 0;
|
|
655
|
+
}
|
|
656
|
+
async function dsarTestCommand(io, client, h) {
|
|
657
|
+
const types = (str(h, "types") ?? "access").split(",").map((t) => t.trim()).filter(Boolean);
|
|
658
|
+
const allowed = ["access", "deletion", "rectification", "portability", "opt-out"];
|
|
659
|
+
const bad = types.filter((t) => !allowed.includes(t));
|
|
660
|
+
if (bad.length) throw h.usage(`--types takes ${allowed.join(", ")}; not ${bad.join(", ")}`);
|
|
661
|
+
const withCbid = h.flag("with-cbid") === true;
|
|
662
|
+
const without = h.flag("without-cbid") === true || !withCbid;
|
|
663
|
+
const cbid = withCbid ? h.requireSite() : void 0;
|
|
664
|
+
const email = str(h, "email") ?? "cookiemunch-cli-test@example.invalid";
|
|
665
|
+
const created = [];
|
|
666
|
+
let failed = false;
|
|
667
|
+
try {
|
|
668
|
+
for (const type of types) {
|
|
669
|
+
for (const scoped of [...withCbid ? [true] : [], ...without ? [false] : []]) {
|
|
670
|
+
const r = await client.dsar.create({ type, subjectEmail: email, regulation: "gdpr", test: true, ...scoped ? { cbid } : {} });
|
|
671
|
+
const back = await client.dsar.get(r.request.id);
|
|
672
|
+
created.push({ id: r.request.id, type, cbid: scoped ? cbid : null, status: String(back.status) });
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
} catch (e) {
|
|
676
|
+
failed = true;
|
|
677
|
+
io.stderr(`dsar test: ${e.message}
|
|
678
|
+
`);
|
|
679
|
+
}
|
|
680
|
+
const deleted = [];
|
|
681
|
+
if (h.flag("cleanup") === true) {
|
|
682
|
+
for (const c of created) {
|
|
683
|
+
try {
|
|
684
|
+
await client.dsar.delete(c.id);
|
|
685
|
+
deleted.push(c.id);
|
|
686
|
+
} catch (e) {
|
|
687
|
+
failed = true;
|
|
688
|
+
io.stderr(`could not delete test request ${c.id}: ${e.message}
|
|
689
|
+
`);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
if (json(h)) print(io, { created, deleted });
|
|
694
|
+
else {
|
|
695
|
+
for (const c of created) io.stdout(`${c.id} ${c.type}${c.cbid ? ` cbid=${c.cbid}` : " (no cbid)"} ${c.status}${deleted.includes(c.id) ? " (deleted)" : ""}
|
|
696
|
+
`);
|
|
697
|
+
io.stderr(`${created.length} test request(s) filed${h.flag("cleanup") === true ? `, ${deleted.length} deleted` : "; delete them with --cleanup or DELETE /v1/dsar/{id}"}.
|
|
698
|
+
`);
|
|
699
|
+
}
|
|
700
|
+
return failed ? 1 : 0;
|
|
701
|
+
}
|
|
702
|
+
async function doctorCommand(io, client, h, cliVersion) {
|
|
703
|
+
const checks = [];
|
|
704
|
+
let me = null;
|
|
705
|
+
try {
|
|
706
|
+
me = await client.me();
|
|
707
|
+
checks.push({ check: "key", ok: true, detail: `org ${me?.orgName ?? me?.orgId} (${me?.orgId}), key ${me?.keyPrefix}${me?.unscoped ? ", full access" : ""}` });
|
|
708
|
+
} catch (e) {
|
|
709
|
+
checks.push({ check: "key", ok: false, detail: e.message });
|
|
710
|
+
}
|
|
711
|
+
const wantOrg = str(h, "org") ?? io.env.COOKIEMUNCH_ORG;
|
|
712
|
+
if (wantOrg && me?.orgId) checks.push({ check: "org", ok: me.orgId === wantOrg, detail: me.orgId === wantOrg ? `this key belongs to ${wantOrg}` : `this key's org is ${me.orgId}, not ${wantOrg}, the org your config names` });
|
|
713
|
+
try {
|
|
714
|
+
const self = await client.keys.self();
|
|
715
|
+
const left = typeof self.expiresAt === "number" ? Math.floor((self.expiresAt - Date.now()) / 864e5) : null;
|
|
716
|
+
checks.push({ check: "key expiry", ok: left === null ? null : left > 7, detail: left === null ? "no expiry: give automation keys one" : `expires in ${left} day(s)` });
|
|
717
|
+
} catch {
|
|
718
|
+
checks.push({ check: "key expiry", ok: null, detail: "this server cannot read the key itself (GET /v1/keys/self)" });
|
|
719
|
+
}
|
|
720
|
+
try {
|
|
721
|
+
const res = await io.fetch("https://registry.npmjs.org/@cookiemunch/cli/latest");
|
|
722
|
+
const latest = res.ok ? (await res.json()).version : void 0;
|
|
723
|
+
checks.push({ check: "cli version", ok: latest ? latest === cliVersion : null, detail: latest ? latest === cliVersion ? `${cliVersion}, the latest` : `${cliVersion}; ${latest} is out` : `${cliVersion}; could not check the latest` });
|
|
724
|
+
} catch {
|
|
725
|
+
checks.push({ check: "cli version", ok: null, detail: `${cliVersion}; could not check the latest` });
|
|
726
|
+
}
|
|
727
|
+
const site = h.site();
|
|
728
|
+
if (site || me) {
|
|
729
|
+
try {
|
|
730
|
+
const r = await client.integrationHealth(site);
|
|
731
|
+
checks.push({ check: "integration health", ok: r.ok, detail: `${r.counts.error} error(s), ${r.counts.warning} warning(s)` });
|
|
732
|
+
} catch (e) {
|
|
733
|
+
checks.push({ check: "integration health", ok: null, detail: e.message });
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
if (json(h)) print(io, { ok: checks.every((c) => c.ok !== false), checks });
|
|
737
|
+
else for (const c of checks) io.stdout(`${c.ok === true ? "ok " : c.ok === false ? "FAIL" : "-- "} ${c.check}: ${c.detail}
|
|
738
|
+
`);
|
|
739
|
+
return checks.some((c) => c.ok === false) ? 1 : 0;
|
|
740
|
+
}
|
|
741
|
+
function parseTtl(v) {
|
|
742
|
+
const m = /^(\d+)([smh]?)$/.exec(v.trim());
|
|
743
|
+
if (!m) return null;
|
|
744
|
+
const n = Number(m[1]) * (m[2] === "h" ? 3600 : m[2] === "m" ? 60 : 1);
|
|
745
|
+
return n >= 60 && n <= 43200 ? n : null;
|
|
746
|
+
}
|
|
747
|
+
async function keysExtraCommand(sub, io, client, h) {
|
|
748
|
+
if (sub === "self") {
|
|
749
|
+
print(io, await client.keys.self());
|
|
750
|
+
return 0;
|
|
751
|
+
}
|
|
752
|
+
if (sub === "rotate-self") {
|
|
753
|
+
const r = await client.keys.rollSelf();
|
|
754
|
+
if (json(h)) print(io, r);
|
|
755
|
+
else {
|
|
756
|
+
io.stdout(`${r.key ?? r.sealed_secret}
|
|
757
|
+
`);
|
|
758
|
+
io.stderr("The old key stopped working. This new one is shown once: store it now.\n");
|
|
759
|
+
}
|
|
760
|
+
return 0;
|
|
761
|
+
}
|
|
762
|
+
if (sub === "vend") {
|
|
763
|
+
const scopes = (str(h, "scopes") ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
764
|
+
if (!scopes.length) throw h.usage("--scopes a,b is required (a vended key is never full access)");
|
|
765
|
+
const ttl = parseTtl(str(h, "ttl") ?? "");
|
|
766
|
+
if (ttl === null) throw h.usage("--ttl is required: 1h, 30m or seconds, from 60 s to 12 h");
|
|
767
|
+
const cbids = str(h, "cbids")?.split(",").map((s) => s.trim()).filter(Boolean);
|
|
768
|
+
const name = str(h, "name");
|
|
769
|
+
const r = await client.keys.vend({ scopes, ttlSeconds: ttl, ...cbids?.length ? { cbids } : {}, ...name ? { name } : {} });
|
|
770
|
+
if (json(h)) print(io, r);
|
|
771
|
+
else {
|
|
772
|
+
io.stdout(`${r.key ?? r.sealed_secret}
|
|
773
|
+
`);
|
|
774
|
+
io.stderr(`Valid until ${new Date(r.expiresAt).toISOString()} (${scopes.join(", ")}).
|
|
775
|
+
`);
|
|
776
|
+
}
|
|
777
|
+
return 0;
|
|
778
|
+
}
|
|
779
|
+
throw h.usage("usage: cookiemunch keys report|vend|self|rotate-self");
|
|
780
|
+
}
|
|
781
|
+
var PARTS = ["site", "config", "cookies", "presets", "rightsPage", "executors", "webhooks", "partners"];
|
|
782
|
+
async function bundleCommand(sub, io, client, h) {
|
|
783
|
+
const path = h.positional[1] ?? "cookiemunch.json";
|
|
784
|
+
const b = await h.readJson(path);
|
|
785
|
+
for (const k of Object.keys(b)) if (!PARTS.includes(k) && k !== "$schema") throw h.usage(`${path}: unknown part ${k}; parts are ${PARTS.join(", ")}`);
|
|
786
|
+
const cbid = b.site?.cbid ?? h.site();
|
|
787
|
+
if (!cbid) throw h.usage(`${path} needs site.cbid (or --site)`);
|
|
788
|
+
const plans = [];
|
|
789
|
+
if (b.site) {
|
|
790
|
+
const patch = { ...b.site.domain ? { domain: b.site.domain } : {}, ...b.site.name !== void 0 ? { name: b.site.name } : {}, ...b.site.aliases ? { aliases: b.site.aliases } : {} };
|
|
791
|
+
if (Object.keys(patch).length) {
|
|
792
|
+
const r = await client.sites.update(cbid, patch, { dryRun: true });
|
|
793
|
+
plans.push({ part: "site", changes: r.diff.length, detail: r.diff.map((d) => `${d.field}: ${JSON.stringify(d.before)} \u2192 ${JSON.stringify(d.after)}`), apply: async () => {
|
|
794
|
+
await client.sites.update(cbid, patch);
|
|
795
|
+
return "site updated";
|
|
796
|
+
} });
|
|
797
|
+
} else plans.push({ part: "site", changes: 0, detail: [] });
|
|
798
|
+
}
|
|
799
|
+
if (b.config) {
|
|
800
|
+
const r = await client.sites.dryRunConfig(cbid, b.config, { mode: "patch" });
|
|
801
|
+
const diff = r.diff ?? [];
|
|
802
|
+
plans.push({ part: "config", changes: diff.length, detail: diff.map((d) => `${d.path}: ${JSON.stringify(d.from)} \u2192 ${JSON.stringify(d.to)}`), apply: async () => {
|
|
803
|
+
await client.sites.patchConfig(cbid, b.config);
|
|
804
|
+
return "config patched";
|
|
805
|
+
} });
|
|
806
|
+
}
|
|
807
|
+
if (b.cookies) {
|
|
808
|
+
const r = await client.sites.declareCookies(cbid, b.cookies, { dryRun: true });
|
|
809
|
+
const d = r.diff ?? { added: [], removed: [], changed: [] };
|
|
810
|
+
plans.push({ part: "cookies", changes: d.added.length + d.removed.length + d.changed.length, detail: [...d.added.map((c) => `+ ${c.name}`), ...d.removed.map((c) => `- ${c.name}`), ...d.changed.map((c) => `~ ${c.name}`)], apply: async () => {
|
|
811
|
+
const s = await client.sites.declareCookies(cbid, b.cookies);
|
|
812
|
+
return `${s.saved} declaration(s) saved`;
|
|
813
|
+
} });
|
|
814
|
+
}
|
|
815
|
+
for (const p of b.presets ?? []) {
|
|
816
|
+
const r = await client.sites.applyCookiePreset(cbid, p.vendor, p.params ?? {}, { dryRun: true });
|
|
817
|
+
const d = r.diff;
|
|
818
|
+
const n = d ? d.added.length + d.removed.length + d.changed.length : 0;
|
|
819
|
+
plans.push({ part: "presets", changes: n, detail: n ? [`${p.vendor}: ${n} declaration change(s)`] : [], apply: async () => {
|
|
820
|
+
await client.sites.applyCookiePreset(cbid, p.vendor, p.params ?? {});
|
|
821
|
+
return `preset ${p.vendor} applied`;
|
|
822
|
+
} });
|
|
823
|
+
}
|
|
824
|
+
if (b.rightsPage) {
|
|
825
|
+
const r = await client.rightsPage.dryRun(cbid, b.rightsPage, { mode: "put" });
|
|
826
|
+
const diff = r.diff ?? [];
|
|
827
|
+
plans.push({ part: "rightsPage", changes: diff.length, detail: diff.map((d) => d.path), apply: async () => {
|
|
828
|
+
await client.rightsPage.put(cbid, b.rightsPage);
|
|
829
|
+
return "rights page draft saved (publish it with cookiemunch rights-page publish)";
|
|
830
|
+
} });
|
|
831
|
+
}
|
|
832
|
+
if (b.executors) {
|
|
833
|
+
const current = (await client.fulfillment.executors()).map(executorFileView);
|
|
834
|
+
let n = 0;
|
|
835
|
+
const detail = [];
|
|
836
|
+
for (const w of b.executors) {
|
|
837
|
+
for (const k of ["secretKey", "secret", "webhookSecret"]) if (k in w) throw h.usage(`${path}: executors carry no secrets (${k}); use --secret-env`);
|
|
838
|
+
const m = current.find((c) => c.system === w.system);
|
|
839
|
+
const { id: _i, ...want } = w;
|
|
840
|
+
const changes = m ? diffJson(Object.fromEntries(Object.keys(want).map((k) => [k, m[k] ?? null])), want) : diffJson({}, want);
|
|
841
|
+
if (changes.length) {
|
|
842
|
+
n++;
|
|
843
|
+
detail.push(`${m ? "update" : "create"} ${String(w.system)}`);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
plans.push({ part: "executors", changes: n, detail, apply: async () => {
|
|
847
|
+
await executorsFileCommand("push", io, client, { ...h, positional: ["executors", "push", path], readJson: async () => ({ executors: b.executors }) });
|
|
848
|
+
return "executors pushed";
|
|
849
|
+
} });
|
|
850
|
+
}
|
|
851
|
+
if (b.webhooks) {
|
|
852
|
+
const have = await client.webhooks.list();
|
|
853
|
+
const missing = b.webhooks.filter((w) => !have.some((x) => x.url === w.url));
|
|
854
|
+
const changed = b.webhooks.filter((w) => {
|
|
855
|
+
const x = have.find((y) => y.url === w.url);
|
|
856
|
+
return x && JSON.stringify([...x.events].sort()) !== JSON.stringify([...w.events].sort());
|
|
857
|
+
});
|
|
858
|
+
plans.push({
|
|
859
|
+
part: "webhooks",
|
|
860
|
+
changes: missing.length + changed.length,
|
|
861
|
+
detail: [...missing.map((w) => `+ ${w.url}`), ...changed.map((w) => `~ ${w.url} events`)],
|
|
862
|
+
apply: async () => {
|
|
863
|
+
const secretsOut = str(h, "secrets-out");
|
|
864
|
+
if (missing.length && !secretsOut) throw h.usage("creating webhooks shows their signing secrets once: pass --secrets-out <file> (written 0600)");
|
|
865
|
+
const secrets = {};
|
|
866
|
+
for (const w of missing) {
|
|
867
|
+
const s = await client.webhooks.create({ url: w.url, events: w.events, ...w.cbid !== void 0 ? { cbid: w.cbid } : {} });
|
|
868
|
+
if (s.secret) secrets[w.url] = s.secret;
|
|
869
|
+
}
|
|
870
|
+
for (const w of changed) {
|
|
871
|
+
const x = have.find((y) => y.url === w.url);
|
|
872
|
+
await client.webhooks.update(x.id, { events: w.events });
|
|
873
|
+
}
|
|
874
|
+
if (secretsOut && Object.keys(secrets).length) await io.writeFile(secretsOut, `${JSON.stringify(secrets, null, 2)}
|
|
875
|
+
`, 384);
|
|
876
|
+
return `${missing.length} created, ${changed.length} updated`;
|
|
877
|
+
}
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
if (b.partners) {
|
|
881
|
+
const have = await client.partnerLinks.list();
|
|
882
|
+
const missing = b.partners.filter((p) => !have.some((l) => l.partner === p.partner && l.resource === p.resource && l.status !== "revoked"));
|
|
883
|
+
plans.push({ part: "partners", changes: missing.length, detail: missing.map((p) => `+ ${p.partner} on ${p.resource} (${p.scopes.join(", ")})`), apply: async () => {
|
|
884
|
+
for (const p of missing) await client.partnerLinks.create(p);
|
|
885
|
+
return `${missing.length} link(s) created`;
|
|
886
|
+
} });
|
|
887
|
+
}
|
|
888
|
+
const total = plans.reduce((n, p) => n + p.changes, 0);
|
|
889
|
+
if (sub === "plan") {
|
|
890
|
+
if (json(h)) print(io, { file: path, cbid, changes: total, parts: plans.map(({ apply: _a, ...p }) => p) });
|
|
891
|
+
else for (const p of plans) {
|
|
892
|
+
io.stdout(`${p.part}: ${p.changes ? `${p.changes} change(s)` : "No changes"}
|
|
893
|
+
`);
|
|
894
|
+
for (const d of p.detail) io.stdout(` ${d}
|
|
895
|
+
`);
|
|
896
|
+
}
|
|
897
|
+
if (!json(h)) io.stderr(total ? `${total} change(s) in ${plans.filter((p) => p.changes).length} part(s). Apply with: cookiemunch apply ${path}
|
|
898
|
+
` : `No changes: all ${plans.length} part(s) match.
|
|
899
|
+
`);
|
|
900
|
+
return total && h.flag("exit-code") === true ? 1 : 0;
|
|
901
|
+
}
|
|
902
|
+
const log = [];
|
|
903
|
+
for (const p of plans) {
|
|
904
|
+
if (!p.changes || !p.apply) continue;
|
|
905
|
+
const result = await p.apply();
|
|
906
|
+
log.push({ part: p.part, at: (/* @__PURE__ */ new Date()).toISOString(), result });
|
|
907
|
+
if (!json(h)) io.stderr(`${log[log.length - 1].at} ${p.part}: ${result}
|
|
908
|
+
`);
|
|
909
|
+
}
|
|
910
|
+
if (json(h)) print(io, { file: path, cbid, applied: log });
|
|
911
|
+
else if (!log.length) io.stderr("No changes: nothing to apply.\n");
|
|
912
|
+
return 0;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// src/cli.ts
|
|
916
|
+
var DEFAULT_BASE_URL = "https://api.cookiemunch.net";
|
|
917
|
+
var USAGE = `cookiemunch \u2014 Cookie Munch from the command line
|
|
918
|
+
|
|
919
|
+
Usage:
|
|
920
|
+
cookiemunch config pull --site <cbid> [--out <file>]
|
|
921
|
+
Write the site's config as JSON (to stdout, or to <file>).
|
|
922
|
+
cookiemunch config diff --site <cbid> <file> [--replace] [--exit-code]
|
|
923
|
+
Show what pushing <file> would change (the server's dry run; nothing is stored).
|
|
924
|
+
Read-only fields a pulled file carries (effective, designWarnings, ...) are ignored.
|
|
925
|
+
cookiemunch config push --site <cbid> <file> [--replace] [--strict]
|
|
926
|
+
Apply <file>. Default: merged over the stored config (PATCH) \u2014 fields the file
|
|
927
|
+
leaves out keep their value, and a field set to null is removed (it takes its
|
|
928
|
+
default). --replace sends it whole (PUT): a top-level field the file leaves out
|
|
929
|
+
reverts to its default. Prints any design warnings the server found. --strict
|
|
930
|
+
checks the file first and does not push it (exit 1) when Accept all is pushed
|
|
931
|
+
harder than Reject all anywhere \u2014 for CI.
|
|
932
|
+
cookiemunch config validate --site <cbid> <file> [--replace] [--strict]
|
|
933
|
+
Check <file> for consent-button parity on the server, without writing: is Accept
|
|
934
|
+
all more prominent than Reject all (colour, size, placement, copy, custom CSS)?
|
|
935
|
+
Merged over the stored config like push; --replace checks it as sent. --strict
|
|
936
|
+
exits 1 when there is any warning (notices never fail).
|
|
937
|
+
cookiemunch preview --site <cbid> [--url <page>] [--region <eu|de|us-ca\u2026>] [--lang <de\u2026>]
|
|
938
|
+
[--ttl <seconds>] [--open]
|
|
939
|
+
Print (and with --open, open) a link showing the banner as a visitor in that
|
|
940
|
+
region and language sees it. Works in headless browsers; nothing is recorded.
|
|
941
|
+
cookiemunch executors test <id> (--email <email> | --canary) [--operation <locate|export|erase>]
|
|
942
|
+
Try a connected system against one person without filing a request.
|
|
943
|
+
--operation defaults to locate (lookup is accepted as another name for it).
|
|
944
|
+
--canary uses the organisation's canary address instead: no real person, and
|
|
945
|
+
a canary locate also updates the connection's health. Exits 1 when not ok.
|
|
946
|
+
cookiemunch executors webhook-test <id> --headers-file <json> --body-file <file>
|
|
947
|
+
Would this webhook delivery verify? Sends the headers (a JSON object) and the
|
|
948
|
+
body file byte for byte to the receiver's check; nothing is acted on. Prints
|
|
949
|
+
{ verified, stage, reason } and exits 1 when it does not verify.
|
|
950
|
+
cookiemunch executors deliveries <id>
|
|
951
|
+
The last 50 deliveries to that connection's webhook, each with its reason.
|
|
952
|
+
cookiemunch executors presets
|
|
953
|
+
The versioned presets, which are usable now and which wait on another
|
|
954
|
+
platform, and the organisation's canary address.
|
|
955
|
+
cookiemunch executors conformance <baseUrl> --secret-env <NAME> [--canary-email <addr>]
|
|
956
|
+
[--prefix /privacy] [--json]
|
|
957
|
+
Check any system against the open cm-standard-v2 executor contract, rule by
|
|
958
|
+
rule, using only the canary subject. The credential is read from the
|
|
959
|
+
environment variable NAME, never from the command line. Without
|
|
960
|
+
--canary-email the organisation's canary address is fetched (needs
|
|
961
|
+
COOKIEMUNCH_API_KEY); with it, no Cookie Munch key is needed at all.
|
|
962
|
+
Exits 1 when any rule fails.
|
|
963
|
+
cookiemunch executors conformance --connection <id> [--json]
|
|
964
|
+
The same suite, run by Cookie Munch against a connected system.
|
|
965
|
+
cookiemunch executors roll-completion-secret <id>
|
|
966
|
+
A new completion secret for that connection, printed once; the old one keeps
|
|
967
|
+
verifying for 24 hours.
|
|
968
|
+
cookiemunch executors upgrade-preset <id> [--preset <name>] [--version <n>]
|
|
969
|
+
[--org-id <id>] [--atlas-id-app-id <id>] [--revoke-atlas-id-grant]
|
|
970
|
+
[--accept-preset <path> \u2026] [--dry-run]
|
|
971
|
+
Rebuild a connection from its preset's latest version (or --version), keeping
|
|
972
|
+
its id and credentials; --preset adopts one on a hand-written connection.
|
|
973
|
+
Your hand edits are kept (the diff marks them kept or conflict); --accept-preset
|
|
974
|
+
(repeatable) takes the preset's value for that path. Prints the diff; --dry-run
|
|
975
|
+
changes nothing. A pinned connection is refused.
|
|
976
|
+
cookiemunch executors recheck <id>
|
|
977
|
+
Re-check a connection now (a canary test that records its health): the hook to
|
|
978
|
+
run after deploying that system's rights API. Exits 1 when it is not ok.
|
|
979
|
+
cookiemunch executors health <id> [--limit <n>]
|
|
980
|
+
The connection's last health checks, newest first (up to 50).
|
|
981
|
+
cookiemunch rights-page pull --site <cbid> [--out <file>]
|
|
982
|
+
Write the site's rights page draft as JSON, and print its updatedAt for --base.
|
|
983
|
+
cookiemunch rights-page diff --site <cbid> <file> [--exit-code]
|
|
984
|
+
Validate <file> on the server (dry run) and show what pushing it would change.
|
|
985
|
+
cookiemunch rights-page push --site <cbid> <file> [--base <updatedAt>]
|
|
986
|
+
Replace the draft with <file>. Nothing is live until publish. Refused (exit 1)
|
|
987
|
+
if the draft changed since the diff, or since --base (the updatedAt pull printed).
|
|
988
|
+
cookiemunch rights-page publish --site <cbid> [--yes] [--base <updatedAt>]
|
|
989
|
+
Publish the draft (it goes live at once). Asks first; --yes skips the question
|
|
990
|
+
and is required when there is no terminal to ask in. Prints why when the page
|
|
991
|
+
cannot go live. Refused (exit 1) if the draft changed after it was read (or
|
|
992
|
+
since --base); --yes never overrides that.
|
|
993
|
+
|
|
994
|
+
cookiemunch scan --site <cbid> [--wait] [--timeout <seconds>]
|
|
995
|
+
Start a cookie crawl of the site and print its state (JSON). --wait holds on
|
|
996
|
+
until it is done or failed (default up to 120 s, --timeout to change) and exits
|
|
997
|
+
1 when it failed or is still running at the end. A scan already running is
|
|
998
|
+
waited on rather than started twice.
|
|
999
|
+
|
|
1000
|
+
cookiemunch cookies presets --site <cbid>
|
|
1001
|
+
The declaration presets (atlas, forumulate, chatly, cookiemunch, referalli, firstpromoter,
|
|
1002
|
+
partnerstack, tapfiliate, impact; referally is the deprecated old name of referalli): what each takes,
|
|
1003
|
+
what it declared on this site, and whether the vendor's own manifest exists yet.
|
|
1004
|
+
cookiemunch cookies preset <vendor> --site <cbid> [--param k=v \u2026] [--dry-run]
|
|
1005
|
+
Declare what that platform writes (cookies and browser storage), marked as
|
|
1006
|
+
declared by it; run again to re-sync. Atlas: --param frontendApiHost=auth.example.com
|
|
1007
|
+
[--param crossSubdomainCookies=false]. Chatly: --param publicId=ch_web_\u2026 .
|
|
1008
|
+
Referral and affiliate presets (referalli, firstpromoter, partnerstack, tapfiliate, impact):
|
|
1009
|
+
[--param cookieDomain=.example.com] [--param cookieDays=60] [--param category=\u2026];
|
|
1010
|
+
referalli [--param legacyName=false], impact [--param campaignId=\u2026]. A row filed under a
|
|
1011
|
+
disabled category prints a warning and a suggestedConfigPatch. Any other vendor is
|
|
1012
|
+
declared with the API (PUT /v1/sites/{cbid}/cookies).
|
|
1013
|
+
cookiemunch cookies unpreset <vendor> --site <cbid> [--dry-run]
|
|
1014
|
+
Remove every declaration that vendor's preset made.
|
|
1015
|
+
cookiemunch cookies import-manifest <url> --site <cbid> [--file <manifest.json>] [--dry-run]
|
|
1016
|
+
Declare any vendor's cookies and storage from its embed manifest
|
|
1017
|
+
(/.well-known/forgedeck-embed.json), by URL or from a file. Remove it with
|
|
1018
|
+
cookies unpreset <product>.
|
|
1019
|
+
cookiemunch cookies suggestions --site <cbid>
|
|
1020
|
+
What to declare: the platforms detected on the site and each undeclared row.
|
|
1021
|
+
cookiemunch cookies declare-suggestions --site <cbid> [--names a,b] [--dry-run]
|
|
1022
|
+
Declare them: detected vendors' presets, then every other suggestion with a purpose.
|
|
1023
|
+
cookiemunch cookies delete-observed <name> --site <cbid> [--dry-run]
|
|
1024
|
+
Remove every stored row of that name (a name the site never sets). A declaration stays.
|
|
1025
|
+
cookiemunch cookies purge-observed --site <cbid> --since <time> [--until <time>] [--dry-run]
|
|
1026
|
+
Remove every row a real visit first reported in that window (ISO-8601 or epoch ms).
|
|
1027
|
+
Crawled rows and declarations stay. Run with --dry-run first.
|
|
1028
|
+
cookiemunch site update --site <cbid> [--domain <host>] [--name <name> | --clear-name]
|
|
1029
|
+
[--aliases <a,b>] [--dry-run]
|
|
1030
|
+
Rename or move a site. The cbid and consent ledger stay; a domain move clears
|
|
1031
|
+
verification. --dry-run prints the diff and its effects without saving.
|
|
1032
|
+
cookiemunch site brand --site <cbid> [--url <https URL> | <brand.json>] [--dry-run]
|
|
1033
|
+
Import a brand.json into the banner theme (the rights page uses it too). Values the
|
|
1034
|
+
site set itself are kept. With neither --url nor a file, prints the last import.
|
|
1035
|
+
cookiemunch consent-sync status --site <cbid>
|
|
1036
|
+
Whether hosted consent sync is set up (the secret's last four characters, when it was minted).
|
|
1037
|
+
cookiemunch consent-sync rotate --site <cbid>
|
|
1038
|
+
Mint the site's consent-sync signing secret, or replace it (the old one stops at once).
|
|
1039
|
+
Prints the secret alone on stdout, so it can be piped into your secret store: it is shown once.
|
|
1040
|
+
cookiemunch consent-sync remove --site <cbid> --yes
|
|
1041
|
+
Remove it: hosted consent sync stops for every visitor.
|
|
1042
|
+
cookiemunch consent-sync token --site <cbid> --subject <id> [--ttl <seconds>]
|
|
1043
|
+
Sign a test token locally with COOKIEMUNCH_CONSENT_SYNC_SECRET (nothing is sent), e.g. to try
|
|
1044
|
+
GET /api/v1/<cbid>/profile with curl.
|
|
1045
|
+
cookiemunch health [--site <cbid>] [--json] [--fail-on error|warning|never]
|
|
1046
|
+
Every link the site's privacy wiring depends on, with a fix hint for each that
|
|
1047
|
+
fails. Exits 1 when a check fails (checks that could not run, or are waived, do not count).
|
|
1048
|
+
--fail-on error ignores warnings; --fail-on never always exits 0.
|
|
1049
|
+
cookiemunch provision <file> [--dry-run]
|
|
1050
|
+
Wire a product's privacy in one call (site, connections, Atlas, Forumulate) from
|
|
1051
|
+
a JSON file without secrets: the Atlas key and Forumulate token come from
|
|
1052
|
+
COOKIEMUNCH_ATLAS_SECRET_KEY and COOKIEMUNCH_FORUMULATE_TOKEN. Re-running is a
|
|
1053
|
+
no-op. Exits 1 when a step failed.
|
|
1054
|
+
cookiemunch keys report [--unused-days <n>] [--expiring-days <n>] [--json]
|
|
1055
|
+
Key hygiene: keys unused for n days (default 90), keys with broad access, and
|
|
1056
|
+
keys expiring within n days (default 14). Needs a full-access key.
|
|
1057
|
+
cookiemunch partners list|link|unlink --site <cbid> [--partner <id>]
|
|
1058
|
+
[--external-id <id>] [--label <text>] [<linkId>]
|
|
1059
|
+
Grant a partner platform the site's consent events (no secret is created), list
|
|
1060
|
+
the grants, or revoke one. Any partner this deployment knows (forumulate, atlas, chatly,
|
|
1061
|
+
referalli, or one registered for it).
|
|
1062
|
+
cookiemunch csp --merge <vendors|urls> [--framework next|express|nginx] [--site <cbid>]
|
|
1063
|
+
One Content-Security-Policy from several products' embed manifests
|
|
1064
|
+
(/.well-known/forgedeck-embed.json): cookiemunch, atlas, chatly, forumulate,
|
|
1065
|
+
referalli, or any manifest URL. --framework prints it for next.config.js,
|
|
1066
|
+
Express or nginx; without it, the header value. --site uses your site's own
|
|
1067
|
+
Cookie Munch manifest (its logo, fonts, sync endpoint). Needs no API key. Exits 1
|
|
1068
|
+
when a named product has not published a manifest (the rest are still merged).
|
|
1069
|
+
cookiemunch cookies diff|push <file> --site <cbid> [--replace] [--exit-code]
|
|
1070
|
+
Declarations as a file ({"cookies": [...]}), through the server's dry run. push applies
|
|
1071
|
+
only when something differs; --replace also removes declarations the file leaves out.
|
|
1072
|
+
cookiemunch executors pull [<file>] | diff <file> | push <file> [--secret-env NAME]
|
|
1073
|
+
Connected systems as a file. The file never carries a secret: a new connection's API key
|
|
1074
|
+
comes from the environment variable --secret-env names.
|
|
1075
|
+
cookiemunch dsar test [--types access,deletion] [--with-cbid --site <cbid>] [--without-cbid]
|
|
1076
|
+
[--email <addr>] [--cleanup]
|
|
1077
|
+
File TEST rights requests and read each back. --cleanup deletes exactly the ones this run
|
|
1078
|
+
filed (by id; it never deletes by listing).
|
|
1079
|
+
cookiemunch doctor [--site <cbid>] [--org <id>]
|
|
1080
|
+
The key (and whose org it is, against --org or COOKIEMUNCH_ORG), its expiry, the CLI
|
|
1081
|
+
version against the latest, and integration health. Exits 1 when a check fails.
|
|
1082
|
+
cookiemunch keys vend --scopes <a,b> --ttl <1h|30m|seconds> [--cbids <x,y>] [--name <n>]
|
|
1083
|
+
A short-lived key (at most 12 h) within this key's own scopes; prints the key alone.
|
|
1084
|
+
Needs keys:write (or a full-access key).
|
|
1085
|
+
cookiemunch keys self | rotate-self
|
|
1086
|
+
This key's own prefix, scopes and expiry; or rotate it (the new key is printed once).
|
|
1087
|
+
cookiemunch plan [cookiemunch.json] [--exit-code] [--json]
|
|
1088
|
+
cookiemunch apply [cookiemunch.json] [--secret-env NAME] [--secrets-out <file>]
|
|
1089
|
+
One file for a site: {site, config, cookies, presets, executors, webhooks, rightsPage,
|
|
1090
|
+
partners}, no secrets. plan prints the server's dry run of every part ("No changes" when
|
|
1091
|
+
it matches); apply applies them in dependency order and logs each. New webhook secrets
|
|
1092
|
+
go to --secrets-out (mode 0600).
|
|
1093
|
+
cookiemunch --version | version [--json]
|
|
1094
|
+
The CLI version; --json adds the API version the server documents.
|
|
1095
|
+
|
|
1096
|
+
Every command takes --json for machine-readable output.
|
|
1097
|
+
|
|
1098
|
+
Exit codes:
|
|
1099
|
+
0 ok, or nothing to change
|
|
1100
|
+
1 a difference (with --exit-code), a failure, or a failed check
|
|
1101
|
+
2 a usage mistake
|
|
1102
|
+
|
|
1103
|
+
Environment:
|
|
1104
|
+
COOKIEMUNCH_API_KEY an API key (cookiemunch_live_\u2026, or a legacy fck_\u2026). Required.
|
|
1105
|
+
COOKIEMUNCH_BASE_URL the API origin. Default ${DEFAULT_BASE_URL}.
|
|
1106
|
+
COOKIEMUNCH_SITE default for --site.
|
|
1107
|
+
COOKIEMUNCH_STRICT_FIELDS=1 send Prefer: strict-unknown-fields on writes: a field the
|
|
1108
|
+
API does not know is refused (400 unknown_field), not ignored.
|
|
1109
|
+
COOKIEMUNCH_SECRET_RECIPIENT=x25519:<base64> answers carry once-shown secrets sealed to
|
|
1110
|
+
this key (sealed_secret), never in plaintext.
|
|
1111
|
+
COOKIEMUNCH_ORG the org id doctor checks the key against.
|
|
1112
|
+
--idempotency-key <k> send Idempotency-Key on writes: a retry replays the first answer.
|
|
1113
|
+
`;
|
|
1114
|
+
var UsageError = class extends Error {
|
|
1115
|
+
};
|
|
1116
|
+
var BOOLEAN_FLAGS = /* @__PURE__ */ new Set(["replace", "exit-code", "open", "help", "json", "yes", "strict", "wait", "canary", "dry-run", "clear-name", "revoke-atlas-id-grant", "with-cbid", "without-cbid", "cleanup"]);
|
|
1117
|
+
function parseArgs(argv) {
|
|
1118
|
+
const positional = [];
|
|
1119
|
+
const flags = {};
|
|
1120
|
+
for (let i = 0; i < argv.length; i++) {
|
|
1121
|
+
const a = argv[i];
|
|
1122
|
+
if (a === "-h") {
|
|
1123
|
+
flags.help = true;
|
|
1124
|
+
} else if (a.startsWith("--")) {
|
|
1125
|
+
const eq = a.indexOf("=");
|
|
1126
|
+
const name = eq > 0 ? a.slice(2, eq) : a.slice(2);
|
|
1127
|
+
let value;
|
|
1128
|
+
if (eq > 0) value = a.slice(eq + 1);
|
|
1129
|
+
else if (BOOLEAN_FLAGS.has(name)) value = true;
|
|
1130
|
+
else {
|
|
1131
|
+
const v = argv[i + 1];
|
|
1132
|
+
if (v === void 0 || v.startsWith("--")) throw new UsageError(`--${name} needs a value`);
|
|
1133
|
+
value = v;
|
|
1134
|
+
i++;
|
|
1135
|
+
}
|
|
1136
|
+
if ((name === "param" || name === "accept-preset") && typeof value === "string" && typeof flags[name] === "string") flags[name] = `${flags[name]}
|
|
1137
|
+
${value}`;
|
|
1138
|
+
else flags[name] = value;
|
|
1139
|
+
} else {
|
|
1140
|
+
positional.push(a);
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
return { positional, flags };
|
|
1144
|
+
}
|
|
1145
|
+
function str2(flags, name) {
|
|
1146
|
+
const v = flags[name];
|
|
1147
|
+
return typeof v === "string" ? v : void 0;
|
|
1148
|
+
}
|
|
1149
|
+
function requireSite(p, io) {
|
|
1150
|
+
const site = str2(p.flags, "site") ?? io.env.COOKIEMUNCH_SITE;
|
|
1151
|
+
if (!site) throw new UsageError("--site <cbid> is required (or set COOKIEMUNCH_SITE)");
|
|
1152
|
+
return site;
|
|
1153
|
+
}
|
|
1154
|
+
async function readConfigFile(io, path) {
|
|
1155
|
+
if (!path) throw new UsageError("a config file is required");
|
|
1156
|
+
let text;
|
|
1157
|
+
try {
|
|
1158
|
+
text = await io.readFile(path);
|
|
1159
|
+
} catch (e) {
|
|
1160
|
+
throw new UsageError(`cannot read ${path}: ${e.message}`);
|
|
1161
|
+
}
|
|
1162
|
+
let parsed;
|
|
1163
|
+
try {
|
|
1164
|
+
parsed = JSON.parse(text);
|
|
1165
|
+
} catch (e) {
|
|
1166
|
+
throw new UsageError(`${path} is not valid JSON: ${e.message}`);
|
|
1167
|
+
}
|
|
1168
|
+
if (!isPlainObject(parsed)) throw new UsageError(`${path} must hold a JSON object`);
|
|
1169
|
+
return parsed;
|
|
1170
|
+
}
|
|
1171
|
+
function proposed(current, file, replace, cbid) {
|
|
1172
|
+
return replace ? { ...file, cbid } : mergeLikeServer(current, file);
|
|
1173
|
+
}
|
|
1174
|
+
var CLI_VERSION = "0.6.0";
|
|
1175
|
+
function userAgent(io) {
|
|
1176
|
+
return io.env.COOKIEMUNCH_USER_AGENT || `cookiemunch-cli/${CLI_VERSION}`;
|
|
1177
|
+
}
|
|
1178
|
+
async function rawRequest(io, base, key, method, path, body) {
|
|
1179
|
+
const res = await io.fetch(`${base.replace(/\/+$/, "")}/v1${path}`, {
|
|
1180
|
+
method,
|
|
1181
|
+
headers: { Authorization: `Bearer ${key}`, "User-Agent": userAgent(io), ...body !== void 0 ? { "Content-Type": "application/json" } : {} },
|
|
1182
|
+
...body !== void 0 ? { body: JSON.stringify(body) } : {}
|
|
1183
|
+
});
|
|
1184
|
+
const text = await res.text();
|
|
1185
|
+
let data = text;
|
|
1186
|
+
try {
|
|
1187
|
+
data = text ? JSON.parse(text) : void 0;
|
|
1188
|
+
} catch {
|
|
1189
|
+
}
|
|
1190
|
+
if (!res.ok) {
|
|
1191
|
+
const err = isPlainObject(data) ? data : {};
|
|
1192
|
+
throw new CookieMunchApiError2(
|
|
1193
|
+
res.status,
|
|
1194
|
+
typeof err.error === "string" ? err.error : `request failed with status ${res.status}`,
|
|
1195
|
+
typeof err.code === "string" ? err.code : void 0
|
|
1196
|
+
);
|
|
1197
|
+
}
|
|
1198
|
+
return data;
|
|
1199
|
+
}
|
|
1200
|
+
async function configChanges(client, cbid, file, replace) {
|
|
1201
|
+
const body = withoutReadOnly(file);
|
|
1202
|
+
try {
|
|
1203
|
+
const r = await client.sites.dryRunConfig(cbid, body, { mode: replace ? "put" : "patch" });
|
|
1204
|
+
if (r && r.dryRun === true && Array.isArray(r.diff)) {
|
|
1205
|
+
return r.diff.map((c) => {
|
|
1206
|
+
const ch = { path: c.path, kind: c.from === void 0 ? "added" : c.to === void 0 ? "removed" : "changed" };
|
|
1207
|
+
if (c.from !== void 0) ch.from = c.from;
|
|
1208
|
+
if (c.to !== void 0) ch.to = c.to;
|
|
1209
|
+
return ch;
|
|
1210
|
+
});
|
|
1211
|
+
}
|
|
1212
|
+
} catch {
|
|
1213
|
+
}
|
|
1214
|
+
const current = withoutReadOnly(await client.sites.getConfig(cbid));
|
|
1215
|
+
return diffJson(current, proposed(current, body, replace, cbid));
|
|
1216
|
+
}
|
|
1217
|
+
async function configCommand(sub, p, io, client) {
|
|
1218
|
+
const cbid = requireSite(p, io);
|
|
1219
|
+
if (sub === "pull") {
|
|
1220
|
+
const cfg = await client.sites.getConfig(cbid);
|
|
1221
|
+
const text = `${JSON.stringify(cfg, null, 2)}
|
|
1222
|
+
`;
|
|
1223
|
+
const out2 = str2(p.flags, "out");
|
|
1224
|
+
if (out2) {
|
|
1225
|
+
await io.writeFile(out2, text);
|
|
1226
|
+
io.stderr(`Wrote ${cbid}'s config to ${out2}
|
|
1227
|
+
`);
|
|
1228
|
+
} else {
|
|
1229
|
+
io.stdout(text);
|
|
1230
|
+
}
|
|
1231
|
+
return 0;
|
|
1232
|
+
}
|
|
1233
|
+
if (sub === "validate") {
|
|
1234
|
+
const file = await readConfigFile(io, p.positional[2]);
|
|
1235
|
+
const r = await client.sites.lintConfig(cbid, file, p.flags.replace === true ? { replace: true } : {});
|
|
1236
|
+
if (p.flags.json === true) io.stdout(`${JSON.stringify({ cbid, designWarnings: r.designWarnings }, null, 2)}
|
|
1237
|
+
`);
|
|
1238
|
+
else if (r.designWarnings.length) io.stdout(`${formatDesignWarnings(r.designWarnings)}
|
|
1239
|
+
`);
|
|
1240
|
+
io.stderr(`${summaryLine(r.designWarnings)}
|
|
1241
|
+
`);
|
|
1242
|
+
return p.flags.strict === true && countBySeverity(r.designWarnings).warnings > 0 ? 1 : 0;
|
|
1243
|
+
}
|
|
1244
|
+
if (sub === "diff" || sub === "push") {
|
|
1245
|
+
const file = await readConfigFile(io, p.positional[2]);
|
|
1246
|
+
const replace = p.flags.replace === true;
|
|
1247
|
+
if (sub === "push" && p.flags.strict === true) {
|
|
1248
|
+
const lint = await client.sites.lintConfig(cbid, file, replace ? { replace: true } : {});
|
|
1249
|
+
if (countBySeverity(lint.designWarnings).warnings > 0) {
|
|
1250
|
+
io.stderr(`${formatDesignWarnings(lint.designWarnings)}
|
|
1251
|
+
${summaryLine(lint.designWarnings)}
|
|
1252
|
+
not pushed (--strict).
|
|
1253
|
+
`);
|
|
1254
|
+
return 1;
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
const changes = await configChanges(client, cbid, file, replace);
|
|
1258
|
+
if (p.flags.json === true && (sub === "diff" || changes.length === 0)) io.stdout(`${JSON.stringify({ cbid, changes }, null, 2)}
|
|
1259
|
+
`);
|
|
1260
|
+
if (changes.length === 0) {
|
|
1261
|
+
io.stderr(`No changes: ${cbid}'s config already matches.
|
|
1262
|
+
`);
|
|
1263
|
+
return 0;
|
|
1264
|
+
}
|
|
1265
|
+
if (p.flags.json !== true) io.stdout(`${formatChanges(changes)}
|
|
1266
|
+
`);
|
|
1267
|
+
if (sub === "diff") {
|
|
1268
|
+
io.stderr(`${changes.length} change${changes.length === 1 ? "" : "s"} (nothing sent). Apply with: cookiemunch config push --site ${cbid} ${p.positional[2]}${replace ? " --replace" : ""}
|
|
1269
|
+
`);
|
|
1270
|
+
return p.flags["exit-code"] === true ? 1 : 0;
|
|
1271
|
+
}
|
|
1272
|
+
const saved = replace ? await client.sites.putConfig(cbid, { ...file, cbid }) : await client.sites.patchConfig(cbid, file);
|
|
1273
|
+
io.stderr(`Applied ${changes.length} change${changes.length === 1 ? "" : "s"} to ${cbid}.
|
|
1274
|
+
`);
|
|
1275
|
+
const found = Array.isArray(saved?.designWarnings) ? saved.designWarnings : [];
|
|
1276
|
+
if (p.flags.json === true) io.stdout(`${JSON.stringify({ cbid, applied: changes, designWarnings: found }, null, 2)}
|
|
1277
|
+
`);
|
|
1278
|
+
if (found.length) io.stderr(`${formatDesignWarnings(found)}
|
|
1279
|
+
${summaryLine(found)}
|
|
1280
|
+
`);
|
|
1281
|
+
return 0;
|
|
1282
|
+
}
|
|
1283
|
+
throw new UsageError(`unknown config command: ${sub ?? "(none)"} \u2014 use pull, diff, push or validate`);
|
|
1284
|
+
}
|
|
1285
|
+
async function previewCommand(p, io, client) {
|
|
1286
|
+
const cbid = requireSite(p, io);
|
|
1287
|
+
const ttl = str2(p.flags, "ttl");
|
|
1288
|
+
const ttlSeconds = ttl === void 0 ? void 0 : Number(ttl);
|
|
1289
|
+
if (ttlSeconds !== void 0 && (!Number.isInteger(ttlSeconds) || ttlSeconds < 1 || ttlSeconds > 3600)) {
|
|
1290
|
+
throw new UsageError("--ttl must be a whole number of seconds from 1 to 3600");
|
|
1291
|
+
}
|
|
1292
|
+
const r = await client.sites.previewToken(cbid, {
|
|
1293
|
+
...str2(p.flags, "region") ? { region: str2(p.flags, "region") } : {},
|
|
1294
|
+
...str2(p.flags, "lang") ? { lang: str2(p.flags, "lang") } : {},
|
|
1295
|
+
...str2(p.flags, "url") ? { url: str2(p.flags, "url") } : {},
|
|
1296
|
+
...ttlSeconds !== void 0 ? { ttlSeconds } : {}
|
|
1297
|
+
});
|
|
1298
|
+
if (p.flags.json === true) {
|
|
1299
|
+
io.stdout(`${JSON.stringify(r, null, 2)}
|
|
1300
|
+
`);
|
|
1301
|
+
} else if (r.previewUrl) {
|
|
1302
|
+
io.stdout(`${r.previewUrl}
|
|
1303
|
+
`);
|
|
1304
|
+
} else {
|
|
1305
|
+
io.stdout(`${r.token}
|
|
1306
|
+
`);
|
|
1307
|
+
io.stderr("Add ?cm_preview=<token>&cm_region=..&cm_lang=.. to any page of the site.\n");
|
|
1308
|
+
}
|
|
1309
|
+
io.stderr(`Valid until ${new Date(r.expiresAt).toISOString()}.
|
|
1310
|
+
`);
|
|
1311
|
+
if (p.flags.open === true && r.previewUrl) await io.openUrl(r.previewUrl);
|
|
1312
|
+
return 0;
|
|
1313
|
+
}
|
|
1314
|
+
async function consentSyncCommand(sub, p, io, client) {
|
|
1315
|
+
const cbid = requireSite(p, io);
|
|
1316
|
+
if (sub === "status") {
|
|
1317
|
+
io.stdout(`${JSON.stringify(await client.consentSyncSecret.get(cbid), null, 2)}
|
|
1318
|
+
`);
|
|
1319
|
+
return 0;
|
|
1320
|
+
}
|
|
1321
|
+
if (sub === "rotate") {
|
|
1322
|
+
const r = await client.consentSyncSecret.rotate(cbid);
|
|
1323
|
+
io.stdout(p.flags.json === true ? `${JSON.stringify(r, null, 2)}
|
|
1324
|
+
` : `${r.secret}
|
|
1325
|
+
`);
|
|
1326
|
+
io.stderr("This secret is shown once: store it on your server (never in a page). A previous secret stopped working.\n");
|
|
1327
|
+
return 0;
|
|
1328
|
+
}
|
|
1329
|
+
if (sub === "remove") {
|
|
1330
|
+
if (p.flags.yes !== true) throw new UsageError("consent-sync remove stops hosted consent sync for every visitor: add --yes");
|
|
1331
|
+
await client.consentSyncSecret.remove(cbid);
|
|
1332
|
+
io.stderr(`Hosted consent sync is off for ${cbid}.
|
|
1333
|
+
`);
|
|
1334
|
+
return 0;
|
|
1335
|
+
}
|
|
1336
|
+
if (sub === "token") {
|
|
1337
|
+
const secret = io.env.COOKIEMUNCH_CONSENT_SYNC_SECRET;
|
|
1338
|
+
if (!secret) throw new UsageError("set COOKIEMUNCH_CONSENT_SYNC_SECRET to the site's consent-sync secret");
|
|
1339
|
+
const subject = str2(p.flags, "subject");
|
|
1340
|
+
if (!subject) throw new UsageError("--subject <id> is required");
|
|
1341
|
+
const ttl = str2(p.flags, "ttl");
|
|
1342
|
+
try {
|
|
1343
|
+
const token = mintConsentSyncToken({ secret, cbid, subject, ...ttl !== void 0 ? { ttlSeconds: Number(ttl) } : {} });
|
|
1344
|
+
io.stdout(p.flags.json === true ? `${JSON.stringify({ token }, null, 2)}
|
|
1345
|
+
` : `${token}
|
|
1346
|
+
`);
|
|
1347
|
+
} catch (e) {
|
|
1348
|
+
throw new UsageError(e.message);
|
|
1349
|
+
}
|
|
1350
|
+
return 0;
|
|
1351
|
+
}
|
|
1352
|
+
throw new UsageError(`unknown consent-sync command: ${sub ?? "(none)"} \u2014 use status, rotate, remove or token`);
|
|
1353
|
+
}
|
|
1354
|
+
async function executorsCommand(sub, p, io, base, key, client) {
|
|
1355
|
+
if (sub === "webhook-test") {
|
|
1356
|
+
const id2 = p.positional[2];
|
|
1357
|
+
if (!id2) throw new UsageError("executors webhook-test <id> needs the connection id");
|
|
1358
|
+
const headersFile = str2(p.flags, "headers-file");
|
|
1359
|
+
const bodyFile = str2(p.flags, "body-file");
|
|
1360
|
+
if (!headersFile || !bodyFile) throw new UsageError("--headers-file and --body-file are both required");
|
|
1361
|
+
let headers;
|
|
1362
|
+
try {
|
|
1363
|
+
headers = JSON.parse(await io.readFile(headersFile));
|
|
1364
|
+
} catch {
|
|
1365
|
+
throw new UsageError(`${headersFile} is not a JSON object of header name to value`);
|
|
1366
|
+
}
|
|
1367
|
+
if (!isPlainObject(headers)) throw new UsageError(`${headersFile} is not a JSON object of header name to value`);
|
|
1368
|
+
const body = await io.readFile(bodyFile);
|
|
1369
|
+
const r = await client.fulfillment.testExecutorWebhook(id2, { headers, body });
|
|
1370
|
+
io.stdout(`${JSON.stringify(r, null, 2)}
|
|
1371
|
+
`);
|
|
1372
|
+
return r.verified ? 0 : 1;
|
|
1373
|
+
}
|
|
1374
|
+
if (sub === "deliveries") {
|
|
1375
|
+
const id2 = p.positional[2];
|
|
1376
|
+
if (!id2) throw new UsageError("executors deliveries <id> needs the connection id");
|
|
1377
|
+
io.stdout(`${JSON.stringify(await client.fulfillment.executorWebhookDeliveries(id2), null, 2)}
|
|
1378
|
+
`);
|
|
1379
|
+
return 0;
|
|
1380
|
+
}
|
|
1381
|
+
if (sub === "presets") {
|
|
1382
|
+
io.stdout(`${JSON.stringify(await client.fulfillment.executorPresets(), null, 2)}
|
|
1383
|
+
`);
|
|
1384
|
+
return 0;
|
|
1385
|
+
}
|
|
1386
|
+
if (sub === "conformance") return conformanceCommand(p, io, client);
|
|
1387
|
+
if (sub === "roll-completion-secret") {
|
|
1388
|
+
const id2 = p.positional[2];
|
|
1389
|
+
if (!id2) throw new UsageError("executors roll-completion-secret <id> needs the connection id");
|
|
1390
|
+
io.stdout(`${JSON.stringify(await client.fulfillment.rollExecutorCompletionSecret(id2), null, 2)}
|
|
1391
|
+
`);
|
|
1392
|
+
return 0;
|
|
1393
|
+
}
|
|
1394
|
+
if (sub === "upgrade-preset") {
|
|
1395
|
+
const id2 = p.positional[2];
|
|
1396
|
+
if (!id2) throw new UsageError("executors upgrade-preset <id> needs the connection id");
|
|
1397
|
+
const version = str2(p.flags, "version");
|
|
1398
|
+
if (version !== void 0 && !/^[1-9]\d*$/.test(version)) throw new UsageError("--version must be a positive whole number");
|
|
1399
|
+
const preset = str2(p.flags, "preset");
|
|
1400
|
+
const orgId = str2(p.flags, "org-id");
|
|
1401
|
+
const atlasIdAppId = str2(p.flags, "atlas-id-app-id");
|
|
1402
|
+
const accept = str2(p.flags, "accept-preset");
|
|
1403
|
+
const input = {
|
|
1404
|
+
...preset ? { preset } : {},
|
|
1405
|
+
...version ? { presetVersion: Number(version) } : {},
|
|
1406
|
+
...orgId ? { orgId } : {},
|
|
1407
|
+
...atlasIdAppId ? { atlasIdAppId } : {},
|
|
1408
|
+
...p.flags["revoke-atlas-id-grant"] === true ? { revokeAtlasIdGrant: true } : {},
|
|
1409
|
+
...accept ? { acceptPreset: accept.split("\n") } : {}
|
|
1410
|
+
};
|
|
1411
|
+
const r = p.flags["dry-run"] === true ? await client.fulfillment.dryRunUpgradeExecutorPreset(id2, input) : await client.fulfillment.upgradeExecutorPreset(id2, input);
|
|
1412
|
+
io.stdout(`${JSON.stringify(r, null, 2)}
|
|
1413
|
+
`);
|
|
1414
|
+
return 0;
|
|
1415
|
+
}
|
|
1416
|
+
if (sub === "recheck") {
|
|
1417
|
+
const id2 = p.positional[2];
|
|
1418
|
+
if (!id2) throw new UsageError("executors recheck <id> needs the connection id");
|
|
1419
|
+
const r = await client.fulfillment.recheckExecutor(id2);
|
|
1420
|
+
io.stdout(`${JSON.stringify(r, null, 2)}
|
|
1421
|
+
`);
|
|
1422
|
+
return r.ok === false ? 1 : 0;
|
|
1423
|
+
}
|
|
1424
|
+
if (sub === "health") {
|
|
1425
|
+
const id2 = p.positional[2];
|
|
1426
|
+
if (!id2) throw new UsageError("executors health <id> needs the connection id");
|
|
1427
|
+
const limit = str2(p.flags, "limit");
|
|
1428
|
+
if (limit !== void 0 && !/^[1-9]\d*$/.test(limit)) throw new UsageError("--limit must be a whole number from 1 to 50");
|
|
1429
|
+
io.stdout(`${JSON.stringify(await client.fulfillment.executorHealthHistory(id2, limit ? { limit: Number(limit) } : {}), null, 2)}
|
|
1430
|
+
`);
|
|
1431
|
+
return 0;
|
|
1432
|
+
}
|
|
1433
|
+
if (sub !== "test") throw new UsageError(`unknown executors command: ${sub ?? "(none)"} \u2014 use test, webhook-test, deliveries, presets, upgrade-preset, conformance, roll-completion-secret, recheck or health`);
|
|
1434
|
+
const id = p.positional[2];
|
|
1435
|
+
if (!id) throw new UsageError("executors test <id> needs the connection id");
|
|
1436
|
+
const canary = p.flags.canary === true;
|
|
1437
|
+
const email = str2(p.flags, "email");
|
|
1438
|
+
if (canary && email) throw new UsageError("use --email or --canary, not both");
|
|
1439
|
+
if (!email && !canary) throw new UsageError("--email (or --canary) is required");
|
|
1440
|
+
const asked = str2(p.flags, "operation") ?? "locate";
|
|
1441
|
+
const operation = asked === "lookup" ? "locate" : asked;
|
|
1442
|
+
try {
|
|
1443
|
+
const r = await rawRequest(io, base, key, "POST", `/dsar/executors/${encodeURIComponent(id)}/test`, canary ? { canary: true, operation } : { email, operation });
|
|
1444
|
+
io.stdout(`${JSON.stringify(r, null, 2)}
|
|
1445
|
+
`);
|
|
1446
|
+
return canary && r.ok === false ? 1 : 0;
|
|
1447
|
+
} catch (e) {
|
|
1448
|
+
if (e instanceof CookieMunchApiError2 && e.status === 404 && e.message === "not found") {
|
|
1449
|
+
io.stderr("This server does not offer connection tests yet (POST /v1/dsar/executors/{id}/test).\n");
|
|
1450
|
+
return 1;
|
|
1451
|
+
}
|
|
1452
|
+
throw e;
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
function printConformance(io, report, asJson) {
|
|
1456
|
+
if (asJson) {
|
|
1457
|
+
io.stdout(`${JSON.stringify(report, null, 2)}
|
|
1458
|
+
`);
|
|
1459
|
+
} else {
|
|
1460
|
+
for (const r of report.rules) {
|
|
1461
|
+
io.stdout(`${r.status.toUpperCase().padEnd(4)} ${r.id} ${r.title}
|
|
1462
|
+
`);
|
|
1463
|
+
if (r.detail && r.status !== "pass") io.stdout(` ${r.detail}
|
|
1464
|
+
`);
|
|
1465
|
+
}
|
|
1466
|
+
io.stdout(`
|
|
1467
|
+
${report.passed} passed, ${report.failed} failed, ${report.skipped} skipped
|
|
1468
|
+
`);
|
|
1469
|
+
}
|
|
1470
|
+
return report.ok ? 0 : 1;
|
|
1471
|
+
}
|
|
1472
|
+
async function conformanceCommand(p, io, client) {
|
|
1473
|
+
const asJson = p.flags.json === true;
|
|
1474
|
+
const connection = str2(p.flags, "connection");
|
|
1475
|
+
if (connection) {
|
|
1476
|
+
if (!client) throw new UsageError("--connection runs on Cookie Munch: set COOKIEMUNCH_API_KEY");
|
|
1477
|
+
return printConformance(io, await client.fulfillment.executorConformance(connection), asJson);
|
|
1478
|
+
}
|
|
1479
|
+
const baseUrl = p.positional[2];
|
|
1480
|
+
if (!baseUrl) throw new UsageError("executors conformance <baseUrl> --secret-env <NAME> (or --connection <id>)");
|
|
1481
|
+
const secretEnv = str2(p.flags, "secret-env");
|
|
1482
|
+
if (!secretEnv) throw new UsageError("--secret-env <NAME> names the environment variable holding the executor credential");
|
|
1483
|
+
const secret = io.env[secretEnv];
|
|
1484
|
+
if (!secret) throw new UsageError(`the environment variable ${secretEnv} is empty`);
|
|
1485
|
+
let canaryEmail = str2(p.flags, "canary-email");
|
|
1486
|
+
if (!canaryEmail) {
|
|
1487
|
+
if (!client) throw new UsageError("pass --canary-email, or set COOKIEMUNCH_API_KEY to use your organisation's canary address");
|
|
1488
|
+
canaryEmail = (await client.fulfillment.executorPresets()).canaryEmail;
|
|
1489
|
+
}
|
|
1490
|
+
const prefix = str2(p.flags, "prefix");
|
|
1491
|
+
let report;
|
|
1492
|
+
try {
|
|
1493
|
+
report = await runExecutorConformance({ baseUrl, secret, canaryEmail, fetch: io.fetch, ...prefix ? { prefix } : {} });
|
|
1494
|
+
} catch (e) {
|
|
1495
|
+
throw new UsageError(e.message);
|
|
1496
|
+
}
|
|
1497
|
+
return printConformance(io, report, asJson);
|
|
1498
|
+
}
|
|
1499
|
+
async function scanCommand(p, io, client) {
|
|
1500
|
+
const cbid = requireSite(p, io);
|
|
1501
|
+
const timeout = str2(p.flags, "timeout");
|
|
1502
|
+
const timeoutS = timeout === void 0 ? 120 : Number(timeout);
|
|
1503
|
+
if (!Number.isFinite(timeoutS) || timeoutS < 1 || timeoutS > 3600) throw new UsageError("--timeout must be a number of seconds from 1 to 3600");
|
|
1504
|
+
let started = null;
|
|
1505
|
+
try {
|
|
1506
|
+
started = await client.sites.scan(cbid);
|
|
1507
|
+
} catch (e) {
|
|
1508
|
+
if (!(e instanceof CookieMunchApiError2 && e.status === 409)) throw e;
|
|
1509
|
+
io.stderr(`A scan of ${cbid} is already running${p.flags.wait === true ? "; waiting for it" : ""}.
|
|
1510
|
+
`);
|
|
1511
|
+
if (p.flags.wait !== true) return 0;
|
|
1512
|
+
}
|
|
1513
|
+
if (p.flags.wait !== true) {
|
|
1514
|
+
io.stdout(`${JSON.stringify(started, null, 2)}
|
|
1515
|
+
`);
|
|
1516
|
+
return 0;
|
|
1517
|
+
}
|
|
1518
|
+
const r = await client.sites.waitForScan(cbid, { timeoutMs: timeoutS * 1e3 });
|
|
1519
|
+
io.stdout(`${JSON.stringify(r, null, 2)}
|
|
1520
|
+
`);
|
|
1521
|
+
if (r.state === "failed") {
|
|
1522
|
+
io.stderr(`Scan ${r.scanId ?? ""} failed: ${r.error ?? "no reason given"}
|
|
1523
|
+
`);
|
|
1524
|
+
return 1;
|
|
1525
|
+
}
|
|
1526
|
+
if (r.status === "scanning") {
|
|
1527
|
+
io.stderr(`Scan ${r.scanId ?? ""} is still running after ${timeoutS} s.
|
|
1528
|
+
`);
|
|
1529
|
+
return 1;
|
|
1530
|
+
}
|
|
1531
|
+
const n = r.cookieCount ?? 0;
|
|
1532
|
+
const pages = r.pagesVisited !== void 0 ? ` on ${r.pagesVisited} page${r.pagesVisited === 1 ? "" : "s"}` : "";
|
|
1533
|
+
const kept = r.keptFromEarlier ? `, ${r.keptFromEarlier} kept from earlier crawls and visits` : "";
|
|
1534
|
+
const took = r.durationMs !== void 0 ? ` in ${Math.round(r.durationMs / 1e3)} s` : "";
|
|
1535
|
+
const landed = r.finalUrl ? ` (redirected to ${r.finalUrl})` : "";
|
|
1536
|
+
io.stderr(`Scan ${r.scanId ?? ""} done${took}: ${n} cookie${n === 1 ? "" : "s"} seen${pages}${kept}${landed}.
|
|
1537
|
+
`);
|
|
1538
|
+
return 0;
|
|
1539
|
+
}
|
|
1540
|
+
var PRESET_VENDORS = ["atlas", "forumulate", "chatly", "cookiemunch", "referalli", "firstpromoter", "partnerstack", "tapfiliate", "impact"];
|
|
1541
|
+
var PRESET_ALIASES = { referally: "referalli" };
|
|
1542
|
+
function vendorArg(v) {
|
|
1543
|
+
if (v && Object.prototype.hasOwnProperty.call(PRESET_ALIASES, v)) return PRESET_ALIASES[v];
|
|
1544
|
+
if (!v || !PRESET_VENDORS.includes(v)) throw new UsageError(`a vendor is required: ${PRESET_VENDORS.join(", ")}`);
|
|
1545
|
+
return v;
|
|
1546
|
+
}
|
|
1547
|
+
function presetParams(p) {
|
|
1548
|
+
const raw = str2(p.flags, "param");
|
|
1549
|
+
const out2 = {};
|
|
1550
|
+
for (const line of raw ? raw.split("\n") : []) {
|
|
1551
|
+
const eq = line.indexOf("=");
|
|
1552
|
+
if (eq <= 0) throw new UsageError(`--param takes key=value, got "${line}"`);
|
|
1553
|
+
const v = line.slice(eq + 1);
|
|
1554
|
+
const k = line.slice(0, eq);
|
|
1555
|
+
out2[k] = v === "true" ? true : v === "false" ? false : k === "cookieDays" && /^\d+$/.test(v) ? Number(v) : v;
|
|
1556
|
+
}
|
|
1557
|
+
return out2;
|
|
1558
|
+
}
|
|
1559
|
+
async function cookiesCommand(sub, p, io, client) {
|
|
1560
|
+
const dry = p.flags["dry-run"] === true ? { dryRun: true } : void 0;
|
|
1561
|
+
const print2 = (v) => io.stdout(`${JSON.stringify(v, null, 2)}
|
|
1562
|
+
`);
|
|
1563
|
+
if (sub === "presets") {
|
|
1564
|
+
print2(await client.sites.cookiePresets(requireSite(p, io)));
|
|
1565
|
+
return 0;
|
|
1566
|
+
}
|
|
1567
|
+
if (sub === "unpreset" && p.positional[2] && !PRESET_VENDORS.includes(p.positional[2])) {
|
|
1568
|
+
print2(await client.sites.removeCookiePreset(requireSite(p, io), p.positional[2], dry));
|
|
1569
|
+
return 0;
|
|
1570
|
+
}
|
|
1571
|
+
if (sub === "preset" || sub === "unpreset") {
|
|
1572
|
+
const cbid = requireSite(p, io);
|
|
1573
|
+
const vendor = vendorArg(p.positional[2]);
|
|
1574
|
+
if (sub === "unpreset") {
|
|
1575
|
+
print2(await client.sites.removeCookiePreset(cbid, vendor, dry));
|
|
1576
|
+
return 0;
|
|
1577
|
+
}
|
|
1578
|
+
const r = await client.sites.applyCookiePreset(cbid, vendor, presetParams(p), dry);
|
|
1579
|
+
print2(r);
|
|
1580
|
+
if (r.note) io.stderr(`${r.note}
|
|
1581
|
+
`);
|
|
1582
|
+
for (const s of r.skipped ?? []) io.stderr(`skipped ${s.name}: ${s.reason}
|
|
1583
|
+
`);
|
|
1584
|
+
if (r.manifest.status === "waiting_on_vendor") io.stderr(`Built-in rows: ${r.manifest.waitingOn} does not publish a manifest yet (${r.manifest.item}).
|
|
1585
|
+
`);
|
|
1586
|
+
return 0;
|
|
1587
|
+
}
|
|
1588
|
+
if (sub === "import-manifest") {
|
|
1589
|
+
const cbid = requireSite(p, io);
|
|
1590
|
+
const src = p.positional[2];
|
|
1591
|
+
const file = str2(p.flags, "file");
|
|
1592
|
+
if (!src && !file) throw new UsageError("a manifest URL (https://\u2026/.well-known/forgedeck-embed.json) or --file <manifest.json> is required");
|
|
1593
|
+
const input = file ? { manifest: JSON.parse(await io.readFile(file)) } : { url: src };
|
|
1594
|
+
const r = await client.sites.importCookieManifest(cbid, input, dry);
|
|
1595
|
+
print2(r);
|
|
1596
|
+
for (const s of r.skipped ?? []) io.stderr(`skipped ${s.name}: ${s.reason}
|
|
1597
|
+
`);
|
|
1598
|
+
return 0;
|
|
1599
|
+
}
|
|
1600
|
+
if (sub === "suggestions") {
|
|
1601
|
+
const r = await client.sites.cookieSuggestions(requireSite(p, io));
|
|
1602
|
+
print2(r);
|
|
1603
|
+
io.stderr(`${r.suggestions.length} suggestion${r.suggestions.length === 1 ? "" : "s"}; ${r.vendors.length} platform${r.vendors.length === 1 ? "" : "s"} detected.
|
|
1604
|
+
`);
|
|
1605
|
+
return 0;
|
|
1606
|
+
}
|
|
1607
|
+
if (sub === "declare-suggestions") {
|
|
1608
|
+
const cbid = requireSite(p, io);
|
|
1609
|
+
const names = str2(p.flags, "names");
|
|
1610
|
+
const r = await client.sites.declareCookieSuggestions(cbid, names ? { names: names.split(",").map((n) => n.trim()).filter(Boolean) } : {}, dry);
|
|
1611
|
+
print2(r);
|
|
1612
|
+
io.stderr(`${dry ? "Would declare" : "Declared"} ${r.declaredCount} row${r.declaredCount === 1 ? "" : "s"} and ${r.presetsApplied.length} preset${r.presetsApplied.length === 1 ? "" : "s"}.
|
|
1613
|
+
`);
|
|
1614
|
+
return 0;
|
|
1615
|
+
}
|
|
1616
|
+
if (sub === "delete-observed") {
|
|
1617
|
+
const cbid = requireSite(p, io);
|
|
1618
|
+
const name = p.positional[2];
|
|
1619
|
+
if (!name) throw new UsageError("a cookie name is required");
|
|
1620
|
+
if (dry) print2(await client.sites.deleteObservedCookie(cbid, name, dry));
|
|
1621
|
+
else {
|
|
1622
|
+
await client.sites.deleteObservedCookie(cbid, name);
|
|
1623
|
+
io.stderr(`Removed ${name}.
|
|
1624
|
+
`);
|
|
1625
|
+
}
|
|
1626
|
+
return 0;
|
|
1627
|
+
}
|
|
1628
|
+
if (sub === "purge-observed") {
|
|
1629
|
+
const cbid = requireSite(p, io);
|
|
1630
|
+
const since = str2(p.flags, "since");
|
|
1631
|
+
if (!since) throw new UsageError("--since is required (ISO-8601 or epoch ms)");
|
|
1632
|
+
const until = str2(p.flags, "until");
|
|
1633
|
+
const r = await client.sites.purgeObservedCookies(cbid, { since, ...until ? { until } : {} }, dry);
|
|
1634
|
+
print2(r);
|
|
1635
|
+
io.stderr(`${dry ? "Would remove" : "Removed"} ${r.count} row${r.count === 1 ? "" : "s"}.
|
|
1636
|
+
`);
|
|
1637
|
+
return 0;
|
|
1638
|
+
}
|
|
1639
|
+
throw new UsageError(`unknown cookies command: ${sub ?? "(none)"}`);
|
|
1640
|
+
}
|
|
1641
|
+
async function run(argv, io) {
|
|
1642
|
+
if (argv[0] === "--version" || argv[0] === "-V") return versionCommand(io, CLI_VERSION, io.env.COOKIEMUNCH_BASE_URL || DEFAULT_BASE_URL, argv.includes("--json"));
|
|
1643
|
+
let p;
|
|
1644
|
+
try {
|
|
1645
|
+
p = parseArgs(argv);
|
|
1646
|
+
} catch (e) {
|
|
1647
|
+
io.stderr(`${e.message}
|
|
1648
|
+
|
|
1649
|
+
${USAGE}`);
|
|
1650
|
+
return 2;
|
|
1651
|
+
}
|
|
1652
|
+
const [cmd, sub] = p.positional;
|
|
1653
|
+
if (cmd === "version") {
|
|
1654
|
+
return versionCommand(io, CLI_VERSION, io.env.COOKIEMUNCH_BASE_URL || DEFAULT_BASE_URL, p.flags.json === true);
|
|
1655
|
+
}
|
|
1656
|
+
if (!cmd || p.flags.help === true || cmd === "help") {
|
|
1657
|
+
io.stdout(USAGE);
|
|
1658
|
+
return cmd || p.flags.help === true ? 0 : 2;
|
|
1659
|
+
}
|
|
1660
|
+
if (cmd === "executors" && sub === "conformance" && !io.env.COOKIEMUNCH_API_KEY && str2(p.flags, "canary-email")) {
|
|
1661
|
+
try {
|
|
1662
|
+
return await conformanceCommand(p, io, null);
|
|
1663
|
+
} catch (e) {
|
|
1664
|
+
if (e instanceof UsageError) {
|
|
1665
|
+
io.stderr(`${e.message}
|
|
1666
|
+
`);
|
|
1667
|
+
return 2;
|
|
1668
|
+
}
|
|
1669
|
+
throw e;
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
const base = io.env.COOKIEMUNCH_BASE_URL || DEFAULT_BASE_URL;
|
|
1673
|
+
if (cmd === "csp") {
|
|
1674
|
+
try {
|
|
1675
|
+
const merge = str2(p.flags, "merge");
|
|
1676
|
+
const framework = str2(p.flags, "framework");
|
|
1677
|
+
const site = str2(p.flags, "site") ?? io.env.COOKIEMUNCH_SITE;
|
|
1678
|
+
return await cspCommand(
|
|
1679
|
+
{ apiBase: base, ...merge ? { merge } : {}, ...framework ? { framework } : {}, ...site ? { site } : {} },
|
|
1680
|
+
io,
|
|
1681
|
+
(m) => new UsageError(m)
|
|
1682
|
+
);
|
|
1683
|
+
} catch (e) {
|
|
1684
|
+
if (e instanceof UsageError) {
|
|
1685
|
+
io.stderr(`${e.message}
|
|
1686
|
+
|
|
1687
|
+
${USAGE}`);
|
|
1688
|
+
return 2;
|
|
1689
|
+
}
|
|
1690
|
+
io.stderr(`${e.message ?? String(e)}
|
|
1691
|
+
`);
|
|
1692
|
+
return 1;
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
const key = io.env.COOKIEMUNCH_API_KEY;
|
|
1696
|
+
if (!key) {
|
|
1697
|
+
io.stderr("COOKIEMUNCH_API_KEY is not set. Create a key under Developers in the dashboard.\n");
|
|
1698
|
+
return 2;
|
|
1699
|
+
}
|
|
1700
|
+
const client = createCookieMunch({
|
|
1701
|
+
apiKey: key,
|
|
1702
|
+
baseUrl: base,
|
|
1703
|
+
fetch: io.fetch,
|
|
1704
|
+
userAgent: userAgent(io),
|
|
1705
|
+
...io.env.COOKIEMUNCH_STRICT_FIELDS === "1" ? { strictUnknownFields: true } : {},
|
|
1706
|
+
...io.env.COOKIEMUNCH_SECRET_RECIPIENT ? { secretRecipient: io.env.COOKIEMUNCH_SECRET_RECIPIENT } : {},
|
|
1707
|
+
...typeof p.flags["idempotency-key"] === "string" ? { headers: { "Idempotency-Key": p.flags["idempotency-key"] } } : {}
|
|
1708
|
+
});
|
|
1709
|
+
const ops = {
|
|
1710
|
+
requireSite: () => requireSite(p, io),
|
|
1711
|
+
site: () => str2(p.flags, "site") ?? io.env.COOKIEMUNCH_SITE,
|
|
1712
|
+
readJson: (path) => readConfigFile(io, path),
|
|
1713
|
+
usage: (m) => new UsageError(m),
|
|
1714
|
+
flag: (name) => p.flags[name],
|
|
1715
|
+
positional: p.positional
|
|
1716
|
+
};
|
|
1717
|
+
try {
|
|
1718
|
+
if (cmd === "cookies" && (sub === "diff" || sub === "push")) return await cookiesFileCommand(sub, io, client, ops);
|
|
1719
|
+
if (cmd === "executors" && (sub === "pull" || sub === "diff" || sub === "push")) return await executorsFileCommand(sub, io, client, ops);
|
|
1720
|
+
if (cmd === "dsar" && sub === "test") return await dsarTestCommand(io, client, ops);
|
|
1721
|
+
if (cmd === "doctor") return await doctorCommand(io, client, ops, CLI_VERSION);
|
|
1722
|
+
if (cmd === "keys" && sub && sub !== "report") return await keysExtraCommand(sub, io, client, ops);
|
|
1723
|
+
if (cmd === "plan" || cmd === "apply") return await bundleCommand(cmd, io, client, ops);
|
|
1724
|
+
if (cmd === "config") return await configCommand(sub, p, io, client);
|
|
1725
|
+
if (cmd === "preview") return await previewCommand(p, io, client);
|
|
1726
|
+
if (cmd === "scan") return await scanCommand(p, io, client);
|
|
1727
|
+
if (cmd === "consent-sync") return await consentSyncCommand(sub, p, io, client);
|
|
1728
|
+
if (cmd === "cookies") return await cookiesCommand(sub, p, io, client);
|
|
1729
|
+
if (cmd === "executors") return await executorsCommand(sub, p, io, base, key, client);
|
|
1730
|
+
const helpers = {
|
|
1731
|
+
requireSite: () => requireSite(p, io),
|
|
1732
|
+
site: () => str2(p.flags, "site") ?? io.env.COOKIEMUNCH_SITE,
|
|
1733
|
+
readJson: (path) => readConfigFile(io, path),
|
|
1734
|
+
usage: (m) => new UsageError(m),
|
|
1735
|
+
flag: (name) => p.flags[name]
|
|
1736
|
+
};
|
|
1737
|
+
if (cmd === "site") return await siteCommand(sub, io, client, helpers, p.positional[2]);
|
|
1738
|
+
if (cmd === "health") return await healthCommand(io, client, helpers);
|
|
1739
|
+
if (cmd === "provision") return await provisionCommand(sub, io, client, helpers);
|
|
1740
|
+
if (cmd === "keys") return await keysCommand(sub, io, client, helpers);
|
|
1741
|
+
if (cmd === "partners") return await partnersCommand(sub, p.positional.slice(2), io, client, helpers);
|
|
1742
|
+
if (cmd === "rights-page") {
|
|
1743
|
+
return await rightsPageCommand(sub, p, io, client, {
|
|
1744
|
+
requireSite: () => requireSite(p, io),
|
|
1745
|
+
readJson: (path) => readConfigFile(io, path),
|
|
1746
|
+
usage: (m) => new UsageError(m)
|
|
1747
|
+
});
|
|
1748
|
+
}
|
|
1749
|
+
throw new UsageError(`unknown command: ${cmd}`);
|
|
1750
|
+
} catch (e) {
|
|
1751
|
+
if (e instanceof UsageError) {
|
|
1752
|
+
io.stderr(`${e.message}
|
|
1753
|
+
|
|
1754
|
+
${USAGE}`);
|
|
1755
|
+
return 2;
|
|
1756
|
+
}
|
|
1757
|
+
if (e instanceof CookieMunchApiError2) {
|
|
1758
|
+
io.stderr(`API error ${e.status}${e.code ? ` (${e.code})` : ""}: ${e.message}
|
|
1759
|
+
`);
|
|
1760
|
+
return 1;
|
|
1761
|
+
}
|
|
1762
|
+
io.stderr(`${e.message ?? String(e)}
|
|
1763
|
+
`);
|
|
1764
|
+
return 1;
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
export {
|
|
1769
|
+
mergeLikeServer,
|
|
1770
|
+
diffJson,
|
|
1771
|
+
formatChanges,
|
|
1772
|
+
rightsPageCommand,
|
|
1773
|
+
KNOWN_MANIFEST_HOSTS,
|
|
1774
|
+
CSP_FRAMEWORKS,
|
|
1775
|
+
manifestSources,
|
|
1776
|
+
mergeManifests,
|
|
1777
|
+
policyString,
|
|
1778
|
+
renderPolicy,
|
|
1779
|
+
cspCommand,
|
|
1780
|
+
DEFAULT_BASE_URL,
|
|
1781
|
+
USAGE,
|
|
1782
|
+
parseArgs,
|
|
1783
|
+
run
|
|
1784
|
+
};
|
|
1785
|
+
//# sourceMappingURL=chunk-LJ433UVC.js.map
|