@siteoshq/cli 1.1.1 → 1.3.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/README.md +51 -3
- package/dist/cli.js +913 -31
- package/dist/cli.js.map +1 -1
- package/package.json +21 -21
package/dist/cli.js
CHANGED
|
@@ -1,4 +1,438 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// src/services/cookie-verification.ts
|
|
13
|
+
var cookie_verification_exports = {};
|
|
14
|
+
__export(cookie_verification_exports, {
|
|
15
|
+
VerificationInstallation: () => VerificationInstallation,
|
|
16
|
+
assessCookieObservations: () => assessCookieObservations,
|
|
17
|
+
verificationTarget: () => verificationTarget,
|
|
18
|
+
verifyCookieWebsite: () => verifyCookieWebsite
|
|
19
|
+
});
|
|
20
|
+
import { z as z14 } from "zod";
|
|
21
|
+
function verificationTarget(websiteUrl, candidate) {
|
|
22
|
+
const website = new URL(websiteUrl);
|
|
23
|
+
const target = new URL(candidate ?? websiteUrl, website);
|
|
24
|
+
if (target.origin !== website.origin || target.username || target.password || target.hash || !["https:", "http:"].includes(target.protocol))
|
|
25
|
+
throw new Error(
|
|
26
|
+
"Verify a URL on the selected Project environment's origin, without credentials or fragments."
|
|
27
|
+
);
|
|
28
|
+
return target.toString();
|
|
29
|
+
}
|
|
30
|
+
function matchesName(value, names) {
|
|
31
|
+
return Boolean(
|
|
32
|
+
names && (names.exact.includes(value) || names.prefixes.some((prefix) => value.startsWith(prefix)))
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
function assessCookieObservations(input) {
|
|
36
|
+
const issues = [];
|
|
37
|
+
const unknownOrigins = /* @__PURE__ */ new Set();
|
|
38
|
+
const unknownStorage = /* @__PURE__ */ new Set();
|
|
39
|
+
const services = input.envelope.config.services;
|
|
40
|
+
for (const request of input.requests) {
|
|
41
|
+
const matches = services.filter(
|
|
42
|
+
(service) => service.lifecycle && [
|
|
43
|
+
...service.lifecycle.scriptOrigins,
|
|
44
|
+
...service.lifecycle.iframeOrigins,
|
|
45
|
+
...service.lifecycle.pixelOrigins
|
|
46
|
+
].some((url) => new URL(url).origin === request.origin)
|
|
47
|
+
);
|
|
48
|
+
const state = input.observations.find(
|
|
49
|
+
(item) => item.scenario === request.scenario
|
|
50
|
+
)?.state;
|
|
51
|
+
if (matches.length) {
|
|
52
|
+
const allowed = matches.some(
|
|
53
|
+
(service) => state?.services.find((item) => item.key === service.key)?.allowed
|
|
54
|
+
);
|
|
55
|
+
const advancedGoogle = input.envelope.config.integrations.googleConsentMode === "advanced" && matches.every(
|
|
56
|
+
(service) => ["google-analytics", "google-ads"].includes(service.key)
|
|
57
|
+
);
|
|
58
|
+
if (!allowed && !advancedGoogle)
|
|
59
|
+
issues.push({
|
|
60
|
+
code: "request_without_permission",
|
|
61
|
+
scenario: request.scenario,
|
|
62
|
+
detail: request.origin
|
|
63
|
+
});
|
|
64
|
+
} else if (request.origin !== input.firstPartyOrigin && !input.deliveryOrigins.includes(request.origin))
|
|
65
|
+
unknownOrigins.add(request.origin);
|
|
66
|
+
}
|
|
67
|
+
for (const observation of input.observations) {
|
|
68
|
+
for (const service of services) {
|
|
69
|
+
const allowed = observation.state.services.find(
|
|
70
|
+
(item) => item.key === service.key
|
|
71
|
+
)?.allowed;
|
|
72
|
+
const stored = observation.cookies.some(
|
|
73
|
+
(name) => matchesName(name, service.lifecycle?.firstPartyCookies)
|
|
74
|
+
) || observation.localStorage.some(
|
|
75
|
+
(name) => matchesName(name, service.lifecycle?.localStorage)
|
|
76
|
+
);
|
|
77
|
+
if (!allowed && stored)
|
|
78
|
+
issues.push({
|
|
79
|
+
code: "storage_without_permission",
|
|
80
|
+
scenario: observation.scenario,
|
|
81
|
+
detail: service.key
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
for (const name of observation.cookies) {
|
|
85
|
+
if (name !== "siteos_consent" && !services.some(
|
|
86
|
+
(service) => matchesName(name, service.lifecycle?.firstPartyCookies)
|
|
87
|
+
))
|
|
88
|
+
unknownStorage.add(`cookie:${name}`);
|
|
89
|
+
}
|
|
90
|
+
for (const name of observation.localStorage) {
|
|
91
|
+
if (!name.startsWith("siteos-cookie:") && !services.some(
|
|
92
|
+
(service) => matchesName(name, service.lifecycle?.localStorage)
|
|
93
|
+
))
|
|
94
|
+
unknownStorage.add(`localStorage:${name}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
issues,
|
|
99
|
+
unknownOrigins: [...unknownOrigins].sort(),
|
|
100
|
+
unknownStorage: [...unknownStorage].sort()
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
async function verifyCookieWebsite(input) {
|
|
104
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
105
|
+
const issues = [];
|
|
106
|
+
const requests = [];
|
|
107
|
+
const observations = [];
|
|
108
|
+
let envelope = null;
|
|
109
|
+
let scenario = "first-visit";
|
|
110
|
+
let overflow = false;
|
|
111
|
+
const installation = input.installation;
|
|
112
|
+
const browser = await (input.launch ?? (async () => {
|
|
113
|
+
const playwright = await import("playwright");
|
|
114
|
+
try {
|
|
115
|
+
return await playwright[input.browserName].launch({ headless: true });
|
|
116
|
+
} catch {
|
|
117
|
+
throw new Error(
|
|
118
|
+
`The ${input.browserName} browser is unavailable. Run npx playwright@1.61.1 install ${input.browserName}, then retry.`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
}))();
|
|
122
|
+
const readState = async (page) => State.parse(
|
|
123
|
+
await page.evaluate(
|
|
124
|
+
() => window.SiteOSCookie.getDebugState()
|
|
125
|
+
)
|
|
126
|
+
);
|
|
127
|
+
const waitForRuntime = async (page) => {
|
|
128
|
+
await page.waitForFunction(
|
|
129
|
+
() => Boolean(
|
|
130
|
+
window.SiteOSCookie?.getDebugState().resolved
|
|
131
|
+
),
|
|
132
|
+
void 0,
|
|
133
|
+
{ timeout: 15e3 }
|
|
134
|
+
);
|
|
135
|
+
};
|
|
136
|
+
try {
|
|
137
|
+
for (const gpc of [false, true]) {
|
|
138
|
+
const context = await browser.newContext({
|
|
139
|
+
serviceWorkers: "block",
|
|
140
|
+
...gpc ? { extraHTTPHeaders: { "Sec-GPC": "1" } } : {}
|
|
141
|
+
});
|
|
142
|
+
if (gpc)
|
|
143
|
+
await context.addInitScript(
|
|
144
|
+
() => Object.defineProperty(navigator, "globalPrivacyControl", {
|
|
145
|
+
get: () => true
|
|
146
|
+
})
|
|
147
|
+
);
|
|
148
|
+
const page = await context.newPage();
|
|
149
|
+
page.setDefaultTimeout(15e3);
|
|
150
|
+
page.setDefaultNavigationTimeout(25e3);
|
|
151
|
+
const pending = /* @__PURE__ */ new Set();
|
|
152
|
+
page.on("request", (request) => {
|
|
153
|
+
if (!/^https?:/u.test(request.url())) return;
|
|
154
|
+
if (requests.length >= 2e3) {
|
|
155
|
+
overflow = true;
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
requests.push({
|
|
159
|
+
origin: new URL(request.url()).origin,
|
|
160
|
+
type: request.resourceType(),
|
|
161
|
+
scenario
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
page.on("response", (response) => {
|
|
165
|
+
const expectedUrl = new URL(installation.delivery.configUrl);
|
|
166
|
+
const responseUrl = new URL(response.url());
|
|
167
|
+
if (responseUrl.origin !== expectedUrl.origin || responseUrl.pathname !== expectedUrl.pathname)
|
|
168
|
+
return;
|
|
169
|
+
const responseScenario = scenario;
|
|
170
|
+
const reading = (async () => {
|
|
171
|
+
let timeout;
|
|
172
|
+
try {
|
|
173
|
+
const raw = await Promise.race([
|
|
174
|
+
response.body(),
|
|
175
|
+
new Promise((_, reject) => {
|
|
176
|
+
timeout = setTimeout(() => reject(new Error("timeout")), 5e3);
|
|
177
|
+
})
|
|
178
|
+
]);
|
|
179
|
+
if (raw.byteLength > 512 * 1024) throw new Error("size");
|
|
180
|
+
const parsed = Envelope.parse(JSON.parse(raw.toString()));
|
|
181
|
+
if (parsed.publicKey !== installation.publicKey || parsed.revision !== installation.currentPublishedRevision)
|
|
182
|
+
throw new Error("revision");
|
|
183
|
+
envelope = parsed;
|
|
184
|
+
} catch {
|
|
185
|
+
issues.push({
|
|
186
|
+
code: "configuration_mismatch",
|
|
187
|
+
scenario: responseScenario,
|
|
188
|
+
detail: "Configuration is unavailable, invalid, or differs from the published revision."
|
|
189
|
+
});
|
|
190
|
+
} finally {
|
|
191
|
+
clearTimeout(timeout);
|
|
192
|
+
}
|
|
193
|
+
})();
|
|
194
|
+
pending.add(reading);
|
|
195
|
+
void reading.finally(() => pending.delete(reading));
|
|
196
|
+
});
|
|
197
|
+
scenario = gpc ? "gpc-first-visit" : "first-visit";
|
|
198
|
+
try {
|
|
199
|
+
await page.goto(input.url, { waitUntil: "domcontentloaded" });
|
|
200
|
+
if (new URL(page.url()).origin !== new URL(input.url).origin)
|
|
201
|
+
throw new Error("The website redirected to another origin.");
|
|
202
|
+
await waitForRuntime(page);
|
|
203
|
+
await Promise.all([...pending]);
|
|
204
|
+
const capture = async () => {
|
|
205
|
+
await page.waitForTimeout(1500);
|
|
206
|
+
if (new URL(page.url()).origin !== new URL(input.url).origin)
|
|
207
|
+
throw new Error("The website navigated to another origin.");
|
|
208
|
+
await waitForRuntime(page);
|
|
209
|
+
const state = await readState(page);
|
|
210
|
+
if (state.preview || state.resolved?.publicKey !== installation.publicKey || state.resolved?.revision !== installation.currentPublishedRevision)
|
|
211
|
+
issues.push({
|
|
212
|
+
code: "runtime_mismatch",
|
|
213
|
+
scenario,
|
|
214
|
+
detail: "The loaded runtime is a preview or uses another publication."
|
|
215
|
+
});
|
|
216
|
+
if (Number(state.runtimeVersion.split(".")[0]) < 11)
|
|
217
|
+
issues.push({
|
|
218
|
+
code: "runtime_outdated",
|
|
219
|
+
scenario,
|
|
220
|
+
detail: state.runtimeVersion
|
|
221
|
+
});
|
|
222
|
+
observations.push({
|
|
223
|
+
scenario,
|
|
224
|
+
state,
|
|
225
|
+
cookies: [
|
|
226
|
+
...new Set(
|
|
227
|
+
(await context.cookies()).map((cookie) => cookie.name)
|
|
228
|
+
)
|
|
229
|
+
].sort(),
|
|
230
|
+
localStorage: await page.evaluate(
|
|
231
|
+
() => Object.keys(localStorage).sort()
|
|
232
|
+
)
|
|
233
|
+
});
|
|
234
|
+
};
|
|
235
|
+
await capture();
|
|
236
|
+
if (gpc) {
|
|
237
|
+
const state = observations.at(-1).state;
|
|
238
|
+
if (!state.consent.globalPrivacyControl || !state.consent.privacyChoices.saleOrShareOptOut || !state.consent.privacyChoices.targetedAdvertisingOptOut)
|
|
239
|
+
issues.push({
|
|
240
|
+
code: "gpc_not_applied",
|
|
241
|
+
scenario,
|
|
242
|
+
detail: "GPC must be visible and both privacy opt-outs applied."
|
|
243
|
+
});
|
|
244
|
+
} else {
|
|
245
|
+
for (const action of [
|
|
246
|
+
"reject",
|
|
247
|
+
"accept",
|
|
248
|
+
"granular",
|
|
249
|
+
"withdraw"
|
|
250
|
+
]) {
|
|
251
|
+
scenario = action;
|
|
252
|
+
try {
|
|
253
|
+
await page.evaluate(async (choice) => {
|
|
254
|
+
const api = window.SiteOSCookie;
|
|
255
|
+
if (choice === "accept") await api.acceptAll();
|
|
256
|
+
else if (choice === "granular")
|
|
257
|
+
await api.updateConsent(
|
|
258
|
+
api.getConsent().categories.filter((key) => key !== "necessary").slice(0, 1)
|
|
259
|
+
);
|
|
260
|
+
else await api.rejectAll();
|
|
261
|
+
}, action);
|
|
262
|
+
} catch (error) {
|
|
263
|
+
if (!(error instanceof Error) || !/Execution context was destroyed/u.test(error.message))
|
|
264
|
+
throw error;
|
|
265
|
+
}
|
|
266
|
+
await capture();
|
|
267
|
+
const state = observations.at(-1).state;
|
|
268
|
+
if ((action === "reject" || action === "withdraw") && state.consent.categories.some((key) => key !== "necessary"))
|
|
269
|
+
issues.push({
|
|
270
|
+
code: "refusal_not_applied",
|
|
271
|
+
scenario,
|
|
272
|
+
detail: "Optional categories remain granted."
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
scenario = "returning-after-refusal";
|
|
276
|
+
await page.reload({ waitUntil: "domcontentloaded" });
|
|
277
|
+
await capture();
|
|
278
|
+
if (observations.at(-1).state.consent.categories.some((key) => key !== "necessary"))
|
|
279
|
+
issues.push({
|
|
280
|
+
code: "refusal_not_persisted",
|
|
281
|
+
scenario,
|
|
282
|
+
detail: "Refusal was not preserved across reload."
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
} catch {
|
|
286
|
+
issues.push({
|
|
287
|
+
code: "scenario_incomplete",
|
|
288
|
+
scenario,
|
|
289
|
+
detail: "The page or Cookie runtime did not complete this scenario within the time limit."
|
|
290
|
+
});
|
|
291
|
+
} finally {
|
|
292
|
+
await context.close();
|
|
293
|
+
await Promise.all([...pending]);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
} finally {
|
|
297
|
+
await browser.close();
|
|
298
|
+
}
|
|
299
|
+
const assessment = envelope ? assessCookieObservations({
|
|
300
|
+
envelope,
|
|
301
|
+
observations,
|
|
302
|
+
requests,
|
|
303
|
+
firstPartyOrigin: new URL(input.url).origin,
|
|
304
|
+
deliveryOrigins: Object.values(installation.delivery).filter((url) => typeof url === "string").map((url) => new URL(url).origin)
|
|
305
|
+
}) : { issues: [], unknownOrigins: [], unknownStorage: [] };
|
|
306
|
+
issues.push(...assessment.issues);
|
|
307
|
+
if (!envelope)
|
|
308
|
+
issues.push({
|
|
309
|
+
code: "configuration_not_observed",
|
|
310
|
+
scenario: "all",
|
|
311
|
+
detail: "No matching public configuration response was observed."
|
|
312
|
+
});
|
|
313
|
+
if (overflow)
|
|
314
|
+
issues.push({
|
|
315
|
+
code: "request_limit",
|
|
316
|
+
scenario: "all",
|
|
317
|
+
detail: "The request limit was reached; this run is incomplete."
|
|
318
|
+
});
|
|
319
|
+
const config = envelope;
|
|
320
|
+
const status = issues.length ? "failed" : assessment.unknownOrigins.length || assessment.unknownStorage.length || config?.config.integrations.googleConsentMode === "advanced" ? "needs-review" : "passed";
|
|
321
|
+
return {
|
|
322
|
+
schemaVersion: 1,
|
|
323
|
+
status,
|
|
324
|
+
startedAt,
|
|
325
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
326
|
+
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1e3).toISOString(),
|
|
327
|
+
target: {
|
|
328
|
+
origin: new URL(input.url).origin,
|
|
329
|
+
path: new URL(input.url).pathname
|
|
330
|
+
},
|
|
331
|
+
browser: input.browserName,
|
|
332
|
+
publicKey: installation.publicKey,
|
|
333
|
+
revision: installation.currentPublishedRevision,
|
|
334
|
+
region: config?.trustedSignals?.region ?? null,
|
|
335
|
+
issues: [
|
|
336
|
+
...new Map(
|
|
337
|
+
issues.map((issue) => [JSON.stringify(issue), issue])
|
|
338
|
+
).values()
|
|
339
|
+
],
|
|
340
|
+
unknownOrigins: assessment.unknownOrigins,
|
|
341
|
+
unknownStorage: assessment.unknownStorage,
|
|
342
|
+
observations,
|
|
343
|
+
requestCounts: [
|
|
344
|
+
...new Set(requests.map((item) => `${item.scenario} ${item.origin}`))
|
|
345
|
+
].map((key) => ({
|
|
346
|
+
scenario: key.split(" ")[0],
|
|
347
|
+
origin: key.split(" ")[1],
|
|
348
|
+
count: requests.filter(
|
|
349
|
+
(item) => `${item.scenario} ${item.origin}` === key
|
|
350
|
+
).length
|
|
351
|
+
})),
|
|
352
|
+
limits: [
|
|
353
|
+
"One route, fresh local browser, 1.5-second observation per scenario, at most 2000 requests.",
|
|
354
|
+
"Consent actions use the public runtime API. Visual, keyboard and GTM Tag Assistant acceptance remain separate.",
|
|
355
|
+
"Unknown origins/storage need classification. First-party or server-side tracking and delayed/interaction-only resources require a source audit.",
|
|
356
|
+
"Geography is this browser's actual Edge location. Use regions resolve for simulations; do not spoof production geography.",
|
|
357
|
+
"No cookie values, request query strings, bodies, credentials or visitor identifiers are included. A pass covers only the observed scope, not legal compliance."
|
|
358
|
+
]
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
var Names, Service, Envelope, State, VerificationInstallation;
|
|
362
|
+
var init_cookie_verification = __esm({
|
|
363
|
+
"src/services/cookie-verification.ts"() {
|
|
364
|
+
"use strict";
|
|
365
|
+
Names = z14.object({
|
|
366
|
+
exact: z14.array(z14.string()),
|
|
367
|
+
prefixes: z14.array(z14.string())
|
|
368
|
+
});
|
|
369
|
+
Service = z14.object({
|
|
370
|
+
key: z14.string(),
|
|
371
|
+
purposeKey: z14.string(),
|
|
372
|
+
lifecycle: z14.object({
|
|
373
|
+
scriptOrigins: z14.array(z14.string()),
|
|
374
|
+
iframeOrigins: z14.array(z14.string()),
|
|
375
|
+
pixelOrigins: z14.array(z14.string()),
|
|
376
|
+
firstPartyCookies: Names,
|
|
377
|
+
localStorage: Names
|
|
378
|
+
}).optional()
|
|
379
|
+
});
|
|
380
|
+
Envelope = z14.object({
|
|
381
|
+
publicKey: z14.string(),
|
|
382
|
+
revision: z14.number(),
|
|
383
|
+
ruleKey: z14.string(),
|
|
384
|
+
profileKey: z14.string(),
|
|
385
|
+
trustedSignals: z14.object({
|
|
386
|
+
region: z14.object({
|
|
387
|
+
countryCode: z14.string().nullable(),
|
|
388
|
+
subdivisionCode: z14.string().nullable()
|
|
389
|
+
}).optional()
|
|
390
|
+
}).optional(),
|
|
391
|
+
config: z14.object({
|
|
392
|
+
services: z14.array(Service),
|
|
393
|
+
categories: z14.array(z14.object({ key: z14.string(), required: z14.boolean() })),
|
|
394
|
+
integrations: z14.object({ googleConsentMode: z14.string() })
|
|
395
|
+
})
|
|
396
|
+
});
|
|
397
|
+
State = z14.object({
|
|
398
|
+
runtimeVersion: z14.string(),
|
|
399
|
+
preview: z14.boolean(),
|
|
400
|
+
resolved: z14.object({
|
|
401
|
+
publicKey: z14.string(),
|
|
402
|
+
revision: z14.number(),
|
|
403
|
+
ruleKey: z14.string(),
|
|
404
|
+
profileKey: z14.string()
|
|
405
|
+
}).nullable(),
|
|
406
|
+
consent: z14.object({
|
|
407
|
+
categories: z14.array(z14.string()),
|
|
408
|
+
decision: z14.string(),
|
|
409
|
+
globalPrivacyControl: z14.boolean(),
|
|
410
|
+
privacyChoices: z14.object({
|
|
411
|
+
saleOrShareOptOut: z14.boolean(),
|
|
412
|
+
targetedAdvertisingOptOut: z14.boolean()
|
|
413
|
+
})
|
|
414
|
+
}),
|
|
415
|
+
services: z14.array(
|
|
416
|
+
z14.object({
|
|
417
|
+
key: z14.string(),
|
|
418
|
+
allowed: z14.boolean(),
|
|
419
|
+
footprintPresent: z14.boolean()
|
|
420
|
+
})
|
|
421
|
+
)
|
|
422
|
+
});
|
|
423
|
+
VerificationInstallation = z14.object({
|
|
424
|
+
publicKey: z14.string(),
|
|
425
|
+
currentPublishedRevision: z14.number().int().positive(),
|
|
426
|
+
delivery: z14.object({
|
|
427
|
+
runtimeUrl: z14.string().url(),
|
|
428
|
+
configUrl: z14.string().url(),
|
|
429
|
+
analyticsUrl: z14.string().url().nullable(),
|
|
430
|
+
receiptsUrl: z14.string().url(),
|
|
431
|
+
handshakeUrl: z14.string().url()
|
|
432
|
+
})
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
});
|
|
2
436
|
|
|
3
437
|
// src/health-check.ts
|
|
4
438
|
import path10 from "path";
|
|
@@ -49,7 +483,8 @@ var siteOSServiceAudienceSchema = z.enum([
|
|
|
49
483
|
"siteos-cookie",
|
|
50
484
|
"siteos-forms",
|
|
51
485
|
"siteos-pulse",
|
|
52
|
-
"siteos-search"
|
|
486
|
+
"siteos-search",
|
|
487
|
+
"siteos-seo"
|
|
53
488
|
]);
|
|
54
489
|
var siteOSServiceScopeSchema = z.string().min(1).max(160).regex(/^[a-z][a-z0-9-]*(?::[a-z][a-z0-9-]*){2,}$/);
|
|
55
490
|
var scopeNamespaceByAudience = {
|
|
@@ -59,7 +494,8 @@ var scopeNamespaceByAudience = {
|
|
|
59
494
|
"siteos-cookie": "cookie",
|
|
60
495
|
"siteos-forms": "forms",
|
|
61
496
|
"siteos-pulse": "pulse",
|
|
62
|
-
"siteos-search": "search"
|
|
497
|
+
"siteos-search": "search",
|
|
498
|
+
"siteos-seo": "seo"
|
|
63
499
|
};
|
|
64
500
|
function matchingServiceScopes(value, context) {
|
|
65
501
|
const prefix = `${scopeNamespaceByAudience[value.audience]}:`;
|
|
@@ -1008,7 +1444,8 @@ var PROJECT_SERVICES = [
|
|
|
1008
1444
|
"cookie",
|
|
1009
1445
|
"forms",
|
|
1010
1446
|
"search",
|
|
1011
|
-
"trace"
|
|
1447
|
+
"trace",
|
|
1448
|
+
"seo"
|
|
1012
1449
|
];
|
|
1013
1450
|
var ProjectSchema = z4.object({
|
|
1014
1451
|
id: z4.string().min(1),
|
|
@@ -1062,7 +1499,7 @@ var OverviewSchema = z4.object({
|
|
|
1062
1499
|
});
|
|
1063
1500
|
function createProjectApi(input) {
|
|
1064
1501
|
const origin = resolveSiteOSAuthBaseUrl(input.env);
|
|
1065
|
-
async function request(
|
|
1502
|
+
async function request(path30, schema, body, method) {
|
|
1066
1503
|
const scope = body === void 0 ? "projects:workspace:read" : "projects:workspace:write";
|
|
1067
1504
|
const grant = await input.grants.acquire({
|
|
1068
1505
|
audience: "siteos-projects",
|
|
@@ -1076,7 +1513,7 @@ function createProjectApi(input) {
|
|
|
1076
1513
|
message: "SiteOS API access is unavailable."
|
|
1077
1514
|
});
|
|
1078
1515
|
const response = await input.fetchImpl(
|
|
1079
|
-
`${origin}/api/projects/v1/projects${
|
|
1516
|
+
`${origin}/api/projects/v1/projects${path30}`,
|
|
1080
1517
|
{
|
|
1081
1518
|
method: method ?? (body === void 0 ? "GET" : "POST"),
|
|
1082
1519
|
headers: {
|
|
@@ -1272,14 +1709,14 @@ async function commonServiceContext(options, service, environmentSlug) {
|
|
|
1272
1709
|
const binding = attachment.environments.find(
|
|
1273
1710
|
(item) => item.environmentId === context.environment.id
|
|
1274
1711
|
);
|
|
1275
|
-
if (["pulse", "cookie"].includes(service) && !binding)
|
|
1712
|
+
if (["pulse", "cookie", "seo"].includes(service) && !binding)
|
|
1276
1713
|
throw new SiteOSAuthApiError({
|
|
1277
1714
|
code: "PROJECT_ENVIRONMENT_NOT_CONFIGURED",
|
|
1278
1715
|
message: `Set up ${service} in ${context.environment.name} with \`siteos project connect ${service}\`.`
|
|
1279
1716
|
});
|
|
1280
1717
|
return {
|
|
1281
1718
|
...context,
|
|
1282
|
-
resourceId: ["pulse", "cookie"].includes(service) ? binding.resourceId : attachment.resourceId,
|
|
1719
|
+
resourceId: ["pulse", "cookie", "seo"].includes(service) ? binding.resourceId : attachment.resourceId,
|
|
1283
1720
|
environmentBinding: binding
|
|
1284
1721
|
};
|
|
1285
1722
|
}
|
|
@@ -8318,13 +8755,13 @@ Usage:
|
|
|
8318
8755
|
siteos project use <id-or-slug> [--json]
|
|
8319
8756
|
siteos project update [--name <name>] [--slug <slug>] [--url <production-url>] [--json]
|
|
8320
8757
|
siteos project status [--json]
|
|
8321
|
-
siteos project connect <pulse|cookie|forms|search|trace> [--resource <id>] [--json]
|
|
8758
|
+
siteos project connect <pulse|cookie|forms|search|trace|seo> [--resource <id>] [--json]
|
|
8322
8759
|
siteos project environment list [--json]
|
|
8323
8760
|
siteos project environment create --name <name> --slug <slug> [--url <url>] [--json]
|
|
8324
8761
|
siteos project environment use <slug> [--json]
|
|
8325
8762
|
siteos project environment update <slug> [--name <name>] [--url <url>] [--json]
|
|
8326
|
-
siteos project environment resources <pulse|cookie|forms|search|trace> [--json]
|
|
8327
|
-
siteos project environment connect <pulse|cookie|forms|search|trace> --environment <slug> [--resource <id>] [--json]
|
|
8763
|
+
siteos project environment resources <pulse|cookie|forms|search|trace|seo> [--json]
|
|
8764
|
+
siteos project environment connect <pulse|cookie|forms|search|trace|seo> --environment <slug> [--resource <id>] [--json]
|
|
8328
8765
|
|
|
8329
8766
|
Select a Project once per repository. Service commands use its configured resources.
|
|
8330
8767
|
Connect creates a draft workspace, or explicitly attaches an existing resource.
|
|
@@ -8422,7 +8859,7 @@ Run \`siteos project status\` to inspect its services.`;
|
|
|
8422
8859
|
const service = parsed.positionals[0];
|
|
8423
8860
|
if (!PROJECT_SERVICES.includes(service))
|
|
8424
8861
|
throw new Error(
|
|
8425
|
-
"Choose pulse, cookie, forms, search or
|
|
8862
|
+
"Choose pulse, cookie, forms, search, trace or seo. Organization connections use `siteos integrations`. "
|
|
8426
8863
|
);
|
|
8427
8864
|
overview = await api.connect(
|
|
8428
8865
|
overview.project.id,
|
|
@@ -8478,7 +8915,7 @@ async function runEnvironmentCommand3(options) {
|
|
|
8478
8915
|
throw new Error("Environment create requires --name and --slug.");
|
|
8479
8916
|
const service = positionals[0];
|
|
8480
8917
|
if ((action === "connect" || action === "resources") && !PROJECT_SERVICES.includes(service))
|
|
8481
|
-
throw new Error("Choose pulse, cookie, forms, search or
|
|
8918
|
+
throw new Error("Choose pulse, cookie, forms, search, trace or seo.");
|
|
8482
8919
|
if (action === "connect" && !values.environment)
|
|
8483
8920
|
throw new Error("Choose a Project environment with --environment <slug>.");
|
|
8484
8921
|
const context = await readCommonProject(
|
|
@@ -8563,13 +9000,18 @@ async function runEnvironmentCommand3(options) {
|
|
|
8563
9000
|
import { readFile as readFile16, stat } from "fs/promises";
|
|
8564
9001
|
import path28 from "path";
|
|
8565
9002
|
import { parseArgs as parseArgs3 } from "util";
|
|
8566
|
-
import { z as
|
|
9003
|
+
import { z as z15 } from "zod";
|
|
8567
9004
|
var SERVICE_HELP = {
|
|
8568
9005
|
cookie: `Manage Cookie for the selected SiteOS Project.
|
|
8569
9006
|
|
|
8570
9007
|
Usage:
|
|
8571
9008
|
siteos cookie status [--json]
|
|
8572
9009
|
siteos cookie installation [--json]
|
|
9010
|
+
siteos cookie schema [--json]
|
|
9011
|
+
siteos cookie validate --input <draft.json> [--json]
|
|
9012
|
+
siteos cookie regions resolve [--country <ISO>] [--subdivision <code>] [--source <draft|published>] [--json]
|
|
9013
|
+
siteos cookie verify [--url <same-origin-url>] [--browser <chromium|webkit>] [--json]
|
|
9014
|
+
siteos cookie restore --input <restore.json> [--json]
|
|
8573
9015
|
siteos cookie draft get [--json]
|
|
8574
9016
|
siteos cookie draft save --input <draft.json> [--json]
|
|
8575
9017
|
siteos cookie publish --input <publication.json> [--json]
|
|
@@ -8618,7 +9060,12 @@ async function runServiceCommand(service, options) {
|
|
|
8618
9060
|
"range-days": { type: "string" },
|
|
8619
9061
|
query: { type: "string" },
|
|
8620
9062
|
cursor: { type: "string" },
|
|
8621
|
-
channel: { type: "string" }
|
|
9063
|
+
channel: { type: "string" },
|
|
9064
|
+
country: { type: "string" },
|
|
9065
|
+
subdivision: { type: "string" },
|
|
9066
|
+
source: { type: "string" },
|
|
9067
|
+
url: { type: "string" },
|
|
9068
|
+
browser: { type: "string" }
|
|
8622
9069
|
}
|
|
8623
9070
|
});
|
|
8624
9071
|
const [action, subaction] = positionals;
|
|
@@ -8627,6 +9074,11 @@ async function runServiceCommand(service, options) {
|
|
|
8627
9074
|
cookie: {
|
|
8628
9075
|
status: [],
|
|
8629
9076
|
installation: [],
|
|
9077
|
+
schema: [],
|
|
9078
|
+
validate: ["input"],
|
|
9079
|
+
"regions resolve": ["country", "subdivision", "source"],
|
|
9080
|
+
verify: ["url", "browser"],
|
|
9081
|
+
restore: ["input"],
|
|
8630
9082
|
"draft get": [],
|
|
8631
9083
|
"draft save": ["input"],
|
|
8632
9084
|
publish: ["input"],
|
|
@@ -8706,8 +9158,8 @@ async function runServiceCommand(service, options) {
|
|
|
8706
9158
|
);
|
|
8707
9159
|
const result2 = await response.json();
|
|
8708
9160
|
if (!response.ok) {
|
|
8709
|
-
const error =
|
|
8710
|
-
error:
|
|
9161
|
+
const error = z15.object({
|
|
9162
|
+
error: z15.object({ code: z15.string(), message: z15.string().max(500) })
|
|
8711
9163
|
}).safeParse(result2);
|
|
8712
9164
|
throw new SiteOSAuthApiError({
|
|
8713
9165
|
code: error.success ? error.data.error.code : "SERVICE_REQUEST_FAILED",
|
|
@@ -8731,14 +9183,60 @@ async function runServiceCommand(service, options) {
|
|
|
8731
9183
|
if (service === "cookie") {
|
|
8732
9184
|
if (action === "status" && positionals.length === 1)
|
|
8733
9185
|
result = await request(site);
|
|
8734
|
-
else if (action === "
|
|
9186
|
+
else if (action === "schema") result = await request(`${site}/schema`);
|
|
9187
|
+
else if (action === "validate") {
|
|
9188
|
+
result = await request(
|
|
9189
|
+
`${site}/validate`,
|
|
9190
|
+
"POST",
|
|
9191
|
+
await inputFile(),
|
|
9192
|
+
"cookie:workspace:read"
|
|
9193
|
+
);
|
|
9194
|
+
const validation = z15.object({
|
|
9195
|
+
valid: z15.boolean(),
|
|
9196
|
+
draftVersionMatches: z15.boolean().optional()
|
|
9197
|
+
}).parse(result);
|
|
9198
|
+
if (!validation.valid || validation.draftVersionMatches === false)
|
|
9199
|
+
return { exitCode: 2, stdout: JSON.stringify(result, null, 2) };
|
|
9200
|
+
} else if (action === "restore")
|
|
9201
|
+
result = await request(`${site}/restore`, "POST", await inputFile());
|
|
9202
|
+
else if (action === "regions") {
|
|
9203
|
+
const query = new URLSearchParams();
|
|
9204
|
+
for (const key of ["country", "subdivision", "source"])
|
|
9205
|
+
if (values[key]) query.set(key, values[key]);
|
|
9206
|
+
result = await request(`${site}/regions?${query}`);
|
|
9207
|
+
} else if (action === "verify") {
|
|
9208
|
+
const {
|
|
9209
|
+
VerificationInstallation: VerificationInstallation2,
|
|
9210
|
+
verificationTarget: verificationTarget2,
|
|
9211
|
+
verifyCookieWebsite: verifyCookieWebsite2
|
|
9212
|
+
} = await Promise.resolve().then(() => (init_cookie_verification(), cookie_verification_exports));
|
|
9213
|
+
const browserName = values.browser ?? "chromium";
|
|
9214
|
+
if (browserName !== "chromium" && browserName !== "webkit")
|
|
9215
|
+
throw new Error("Choose chromium or webkit.");
|
|
9216
|
+
if (!common.environment.url)
|
|
9217
|
+
throw new Error(
|
|
9218
|
+
"Set the selected Project environment URL before verification."
|
|
9219
|
+
);
|
|
9220
|
+
const url = verificationTarget2(common.environment.url, values.url);
|
|
9221
|
+
result = await verifyCookieWebsite2({
|
|
9222
|
+
installation: VerificationInstallation2.parse(
|
|
9223
|
+
await request(`${site}/installation`)
|
|
9224
|
+
),
|
|
9225
|
+
url,
|
|
9226
|
+
browserName
|
|
9227
|
+
});
|
|
9228
|
+
return {
|
|
9229
|
+
exitCode: result.status === "passed" ? 0 : 1,
|
|
9230
|
+
stdout: JSON.stringify(result, null, 2)
|
|
9231
|
+
};
|
|
9232
|
+
} else if (action === "installation")
|
|
8735
9233
|
result = await request(`${site}/installation`);
|
|
8736
9234
|
else if (action === "draft" && subaction === "get" && positionals.length === 2) {
|
|
8737
|
-
const response =
|
|
8738
|
-
name:
|
|
8739
|
-
hostname:
|
|
8740
|
-
draftVersion:
|
|
8741
|
-
draft:
|
|
9235
|
+
const response = z15.object({
|
|
9236
|
+
name: z15.string(),
|
|
9237
|
+
hostname: z15.string(),
|
|
9238
|
+
draftVersion: z15.number(),
|
|
9239
|
+
draft: z15.unknown()
|
|
8742
9240
|
}).parse(await request(site));
|
|
8743
9241
|
result = {
|
|
8744
9242
|
name: response.name,
|
|
@@ -8770,9 +9268,9 @@ async function runServiceCommand(service, options) {
|
|
|
8770
9268
|
else if (action === "environments" && positionals.length === 1)
|
|
8771
9269
|
result = await request(`${site}/environments`);
|
|
8772
9270
|
else if (action === "report" || action === "installation" || action === "tracking-plan") {
|
|
8773
|
-
const response =
|
|
8774
|
-
environments:
|
|
8775
|
-
|
|
9271
|
+
const response = z15.object({
|
|
9272
|
+
environments: z15.array(
|
|
9273
|
+
z15.object({ id: z15.string(), slug: z15.string() })
|
|
8776
9274
|
)
|
|
8777
9275
|
}).parse(await request(`${site}/environments`));
|
|
8778
9276
|
const environment = common.overview.project.environments.find(
|
|
@@ -8797,11 +9295,11 @@ async function runServiceCommand(service, options) {
|
|
|
8797
9295
|
);
|
|
8798
9296
|
const base = `/environments/${encodeURIComponent(matches[0].id)}`;
|
|
8799
9297
|
if (action === "report" && positionals.length === 1) {
|
|
8800
|
-
const detail =
|
|
8801
|
-
site:
|
|
8802
|
-
id:
|
|
8803
|
-
environments:
|
|
8804
|
-
|
|
9298
|
+
const detail = z15.object({
|
|
9299
|
+
site: z15.object({
|
|
9300
|
+
id: z15.literal(common.resourceId),
|
|
9301
|
+
environments: z15.array(
|
|
9302
|
+
z15.object({ id: z15.string(), evidence: z15.unknown() })
|
|
8805
9303
|
)
|
|
8806
9304
|
})
|
|
8807
9305
|
}).parse(await request(site));
|
|
@@ -8859,7 +9357,383 @@ async function runServiceCommand(service, options) {
|
|
|
8859
9357
|
} catch (cause) {
|
|
8860
9358
|
return {
|
|
8861
9359
|
exitCode: cause instanceof SiteOSAuthApiError ? 1 : 2,
|
|
8862
|
-
stderr: cause instanceof SiteOSAuthApiError || !(cause instanceof
|
|
9360
|
+
stderr: cause instanceof SiteOSAuthApiError || !(cause instanceof z15.ZodError) && cause instanceof Error ? cause.message : "The service returned an invalid response."
|
|
9361
|
+
};
|
|
9362
|
+
}
|
|
9363
|
+
}
|
|
9364
|
+
|
|
9365
|
+
// src/services/seo-command.ts
|
|
9366
|
+
import { writeFile as writeFile8 } from "fs/promises";
|
|
9367
|
+
import path29 from "path";
|
|
9368
|
+
import { parseArgs as parseArgs4 } from "util";
|
|
9369
|
+
import { z as z16 } from "zod";
|
|
9370
|
+
var SEO_HELP = `Audit public HTML in the selected Project environment.
|
|
9371
|
+
|
|
9372
|
+
Usage:
|
|
9373
|
+
siteos seo status [--environment <slug>] [--json]
|
|
9374
|
+
siteos seo audit run [--environment <slug>] [--json]
|
|
9375
|
+
siteos seo audit list [--environment <slug>] [--json]
|
|
9376
|
+
siteos seo audit show <id> [--environment <slug>] [--json]
|
|
9377
|
+
siteos seo audit cancel <id> [--environment <slug>] [--json]
|
|
9378
|
+
siteos seo pages [--audit <id>] [--query <text>] [--url <url>] [--page <number>] [--environment <slug>] [--json]
|
|
9379
|
+
siteos seo issues [--audit <id>] [--rule <id>] [--page <number>] [--environment <slug>] [--json]
|
|
9380
|
+
siteos seo changes [--audit <id>] [--state <new|reopened|still_present|resolved|not_rechecked>] [--page <number>] [--environment <slug>] [--json]
|
|
9381
|
+
siteos seo recheck --audit <id> --url <url> [--environment <slug>] [--json]
|
|
9382
|
+
siteos seo issue <ignore|restore> --audit <id> --url <url> --rule <id> --reason <text> --revision <number> [--environment <slug>] [--json]
|
|
9383
|
+
siteos seo schedule show [--environment <slug>] [--json]
|
|
9384
|
+
siteos seo schedule set --enabled <true|false> --weekday <1-7> --time <HH:mm> --timezone <IANA> --revision <number> [--environment <slug>] [--json]
|
|
9385
|
+
siteos seo notifications retry <notification-id> [--environment <slug>] [--json]
|
|
9386
|
+
siteos seo notifications show [--environment <slug>] [--json]
|
|
9387
|
+
siteos seo notifications destinations [--environment <slug>] [--json]
|
|
9388
|
+
siteos seo notifications set --enabled <true|false> [--destination <candidate-id>] --severity <error|warning> --failures <true|false> --revision <number> [--environment <slug>] [--json]
|
|
9389
|
+
siteos seo export --audit <id> --kind <pages|issues|changes> --format <csv|json> --output <new-file> [--query <text>] [--rule <id>] [--severity <error|warning|notice>] [--state <page-or-change-state>] [--environment <slug>] [--json]
|
|
9390
|
+
|
|
9391
|
+
Schedule and notification writes require an owner/admin and the saved revision (initially 0).
|
|
9392
|
+
Export writes all matching rows to a new file; existing files are never overwritten.
|
|
9393
|
+
Runs are queued. Read audit show until terminal; an accepted run is not a completed check.
|
|
9394
|
+
Recheck accepts a URL observed in the source audit. Cross-page rules require a full audit.
|
|
9395
|
+
Read the current disposition revision before ignore/restore; use 0 if no decision exists.
|
|
9396
|
+
Setup: siteos project connect seo. No crawl runs during setup.`;
|
|
9397
|
+
async function runSeoCommand(options) {
|
|
9398
|
+
if (!options.args.length || options.args.some((arg) => ["--help", "-h"].includes(arg)))
|
|
9399
|
+
return { exitCode: 0, stdout: SEO_HELP };
|
|
9400
|
+
const json = options.args.includes("--json");
|
|
9401
|
+
try {
|
|
9402
|
+
const { positionals, values } = parseArgs4({
|
|
9403
|
+
args: options.args,
|
|
9404
|
+
strict: true,
|
|
9405
|
+
allowPositionals: true,
|
|
9406
|
+
options: {
|
|
9407
|
+
json: { type: "boolean" },
|
|
9408
|
+
environment: { type: "string" },
|
|
9409
|
+
audit: { type: "string" },
|
|
9410
|
+
url: { type: "string" },
|
|
9411
|
+
rule: { type: "string" },
|
|
9412
|
+
reason: { type: "string" },
|
|
9413
|
+
revision: { type: "string" },
|
|
9414
|
+
query: { type: "string" },
|
|
9415
|
+
page: { type: "string" },
|
|
9416
|
+
state: { type: "string" },
|
|
9417
|
+
enabled: { type: "string" },
|
|
9418
|
+
weekday: { type: "string" },
|
|
9419
|
+
time: { type: "string" },
|
|
9420
|
+
timezone: { type: "string" },
|
|
9421
|
+
destination: { type: "string" },
|
|
9422
|
+
severity: { type: "string" },
|
|
9423
|
+
failures: { type: "string" },
|
|
9424
|
+
kind: { type: "string" },
|
|
9425
|
+
format: { type: "string" },
|
|
9426
|
+
output: { type: "string" }
|
|
9427
|
+
}
|
|
9428
|
+
});
|
|
9429
|
+
const route = positionals.slice(0, 2).join(" ");
|
|
9430
|
+
const action = positionals[0];
|
|
9431
|
+
const operations = {
|
|
9432
|
+
status: { flags: [], args: 1 },
|
|
9433
|
+
"schedule show": { flags: [], args: 2 },
|
|
9434
|
+
"schedule set": {
|
|
9435
|
+
flags: ["enabled", "weekday", "time", "timezone", "revision"],
|
|
9436
|
+
args: 2
|
|
9437
|
+
},
|
|
9438
|
+
"notifications retry": { flags: [], args: 3 },
|
|
9439
|
+
"notifications show": { flags: [], args: 2 },
|
|
9440
|
+
"notifications destinations": { flags: [], args: 2 },
|
|
9441
|
+
"notifications set": {
|
|
9442
|
+
flags: ["enabled", "destination", "severity", "failures", "revision"],
|
|
9443
|
+
args: 2
|
|
9444
|
+
},
|
|
9445
|
+
export: {
|
|
9446
|
+
flags: [
|
|
9447
|
+
"audit",
|
|
9448
|
+
"kind",
|
|
9449
|
+
"format",
|
|
9450
|
+
"output",
|
|
9451
|
+
"query",
|
|
9452
|
+
"rule",
|
|
9453
|
+
"severity",
|
|
9454
|
+
"state"
|
|
9455
|
+
],
|
|
9456
|
+
args: 1
|
|
9457
|
+
},
|
|
9458
|
+
"audit run": { flags: [], args: 2 },
|
|
9459
|
+
"audit list": { flags: [], args: 2 },
|
|
9460
|
+
"audit show": { flags: [], args: 3 },
|
|
9461
|
+
"audit cancel": { flags: [], args: 3 },
|
|
9462
|
+
pages: { flags: ["audit", "url", "query", "page"], args: 1 },
|
|
9463
|
+
issues: { flags: ["audit", "rule", "page"], args: 1 },
|
|
9464
|
+
changes: { flags: ["audit", "state", "page"], args: 1 },
|
|
9465
|
+
recheck: { flags: ["audit", "url"], args: 1 },
|
|
9466
|
+
"issue ignore": {
|
|
9467
|
+
flags: ["audit", "url", "rule", "reason", "revision"],
|
|
9468
|
+
args: 2
|
|
9469
|
+
},
|
|
9470
|
+
"issue restore": {
|
|
9471
|
+
flags: ["audit", "url", "rule", "reason", "revision"],
|
|
9472
|
+
args: 2
|
|
9473
|
+
}
|
|
9474
|
+
};
|
|
9475
|
+
const operation = operations[route];
|
|
9476
|
+
if (!operation || positionals.length !== operation.args || Object.keys(values).some(
|
|
9477
|
+
(key) => !["json", "environment", ...operation.flags].includes(key)
|
|
9478
|
+
))
|
|
9479
|
+
throw new Error(
|
|
9480
|
+
"Invalid SEO operation or flags. Run `siteos seo --help`."
|
|
9481
|
+
);
|
|
9482
|
+
if (action === "recheck" && (!values.audit || !values.url))
|
|
9483
|
+
throw new Error("Recheck requires --audit and --url.");
|
|
9484
|
+
if (action === "issue" && ["audit", "url", "rule", "reason", "revision"].some(
|
|
9485
|
+
(key) => !values[key]
|
|
9486
|
+
))
|
|
9487
|
+
throw new Error(
|
|
9488
|
+
"Issue decisions require --audit, --url, --rule, --reason and --revision."
|
|
9489
|
+
);
|
|
9490
|
+
if (values.page && !/^[1-9]\d{0,5}$/u.test(values.page))
|
|
9491
|
+
throw new Error("Use a positive page number.");
|
|
9492
|
+
if (values.revision && !/^\d{1,9}$/u.test(values.revision))
|
|
9493
|
+
throw new Error("Use a non-negative revision.");
|
|
9494
|
+
if (values.reason && (values.reason.trim().length < 3 || values.reason.length > 500))
|
|
9495
|
+
throw new Error("Use a reason between 3 and 500 characters.");
|
|
9496
|
+
const pageStateFilter = action === "export" && values.kind !== "changes";
|
|
9497
|
+
if (values.state && !(pageStateFilter ? ["analyzed", "unavailable", "excluded", "non_html"] : ["new", "reopened", "still_present", "resolved", "not_rechecked"]).includes(values.state))
|
|
9498
|
+
throw new Error("Use a documented page or change state for this export.");
|
|
9499
|
+
const automation = ["schedule", "notifications"].includes(action ?? "");
|
|
9500
|
+
const setting = automation && positionals[1] === "set";
|
|
9501
|
+
if (setting && (!values.revision || !["true", "false"].includes(values.enabled ?? "")))
|
|
9502
|
+
throw new Error("Settings require --enabled true|false and --revision.");
|
|
9503
|
+
if (route === "schedule set") {
|
|
9504
|
+
if (!/^[1-7]$/u.test(values.weekday ?? "") || !/^([01]\d|2[0-3]):[0-5]\d$/u.test(values.time ?? "") || !values.timezone)
|
|
9505
|
+
throw new Error("Use weekday 1\u20137, HH:mm and an IANA time zone.");
|
|
9506
|
+
try {
|
|
9507
|
+
new Intl.DateTimeFormat("en", { timeZone: values.timezone });
|
|
9508
|
+
} catch {
|
|
9509
|
+
throw new Error("Use a valid IANA time zone.");
|
|
9510
|
+
}
|
|
9511
|
+
}
|
|
9512
|
+
if (route === "notifications set" && (!["error", "warning"].includes(values.severity ?? "") || !["true", "false"].includes(values.failures ?? "") || values.enabled === "true" && !values.destination))
|
|
9513
|
+
throw new Error(
|
|
9514
|
+
"Notifications require --severity, --failures and an available --destination when enabled."
|
|
9515
|
+
);
|
|
9516
|
+
if (action === "export" && (!values.audit || !["pages", "issues", "changes"].includes(values.kind ?? "") || !["csv", "json"].includes(values.format ?? "") || !values.output))
|
|
9517
|
+
throw new Error(
|
|
9518
|
+
"Export requires --audit, --kind, --format and --output."
|
|
9519
|
+
);
|
|
9520
|
+
if (values.severity && !["error", "warning", "notice"].includes(values.severity))
|
|
9521
|
+
throw new Error("Use a documented severity.");
|
|
9522
|
+
const context = await commonServiceContext(
|
|
9523
|
+
options,
|
|
9524
|
+
"seo",
|
|
9525
|
+
values.environment
|
|
9526
|
+
);
|
|
9527
|
+
if (!context)
|
|
9528
|
+
throw new Error(
|
|
9529
|
+
"Select a SiteOS Project with `siteos project use` first."
|
|
9530
|
+
);
|
|
9531
|
+
const runtime = commonProjectRuntime(options);
|
|
9532
|
+
const retryNotification = route === "notifications retry";
|
|
9533
|
+
const writing = retryNotification || setting || [
|
|
9534
|
+
"audit run",
|
|
9535
|
+
"audit cancel",
|
|
9536
|
+
"recheck",
|
|
9537
|
+
"issue ignore",
|
|
9538
|
+
"issue restore"
|
|
9539
|
+
].includes(route);
|
|
9540
|
+
const scope = retryNotification ? "seo:notifications:write" : setting ? action === "schedule" ? "seo:schedule:write" : "seo:notifications:write" : !writing ? "seo:workspace:read" : action === "issue" ? "seo:issues:write" : "seo:audits:write";
|
|
9541
|
+
const grant = await runtime.grants.acquire({
|
|
9542
|
+
audience: "siteos-seo",
|
|
9543
|
+
scopes: [scope]
|
|
9544
|
+
});
|
|
9545
|
+
if (grant.grant.audience !== "siteos-seo" || grant.grant.scopes.length !== 1 || grant.grant.scopes[0] !== scope || grant.grant.organizationId !== context.overview.project.organizationId)
|
|
9546
|
+
throw new SiteOSAuthApiError({
|
|
9547
|
+
code: "AUTH_INVALID_RESPONSE",
|
|
9548
|
+
message: "The SEO grant does not match this Project and operation."
|
|
9549
|
+
});
|
|
9550
|
+
const query = new URLSearchParams();
|
|
9551
|
+
for (const [flag, name] of [
|
|
9552
|
+
["audit", "audit"],
|
|
9553
|
+
["rule", "rule"],
|
|
9554
|
+
["query", "q"],
|
|
9555
|
+
["url", "pageUrl"],
|
|
9556
|
+
["page", "page"],
|
|
9557
|
+
["state", "change"],
|
|
9558
|
+
["kind", "kind"],
|
|
9559
|
+
["format", "format"],
|
|
9560
|
+
["severity", "severity"]
|
|
9561
|
+
])
|
|
9562
|
+
if (values[flag] && !writing) query.set(name, values[flag]);
|
|
9563
|
+
if (pageStateFilter && values.state) {
|
|
9564
|
+
query.delete("change");
|
|
9565
|
+
query.set("state", values.state);
|
|
9566
|
+
}
|
|
9567
|
+
if (route === "audit show") query.set("audit", positionals[2]);
|
|
9568
|
+
const suffix = retryNotification ? "/notification-retries" : automation ? setting ? `/${action}` : route === "notifications destinations" ? "/destinations" : "/automation" : action === "export" ? `/export?${query}` : route === "audit run" ? "/audits" : route === "audit cancel" ? `/audits/${encodeURIComponent(positionals[2])}/cancel` : action === "recheck" ? "/rechecks" : action === "issue" ? "/dispositions" : `?${query}`;
|
|
9569
|
+
const body = retryNotification ? { notificationId: positionals[2] } : route === "schedule set" ? {
|
|
9570
|
+
enabled: values.enabled === "true",
|
|
9571
|
+
weekday: Number(values.weekday),
|
|
9572
|
+
time: values.time,
|
|
9573
|
+
timeZone: values.timezone,
|
|
9574
|
+
expectedRevision: Number(values.revision)
|
|
9575
|
+
} : route === "notifications set" ? {
|
|
9576
|
+
enabled: values.enabled === "true",
|
|
9577
|
+
candidateId: values.destination ?? null,
|
|
9578
|
+
minimumSeverity: values.severity,
|
|
9579
|
+
includeFailures: values.failures === "true",
|
|
9580
|
+
expectedRevision: Number(values.revision)
|
|
9581
|
+
} : action === "recheck" ? { auditId: values.audit, urls: [values.url] } : action === "issue" ? {
|
|
9582
|
+
auditId: values.audit,
|
|
9583
|
+
url: values.url,
|
|
9584
|
+
ruleId: values.rule,
|
|
9585
|
+
reason: values.reason,
|
|
9586
|
+
expectedRevision: Number(values.revision),
|
|
9587
|
+
ignored: positionals[1] === "ignore"
|
|
9588
|
+
} : void 0;
|
|
9589
|
+
if (!options.fetchImpl)
|
|
9590
|
+
throw new Error("SiteOS API access is unavailable.");
|
|
9591
|
+
const response = await options.fetchImpl(
|
|
9592
|
+
`${runtime.api.origin}/api/seo/v1/resources/${encodeURIComponent(context.resourceId)}${suffix}`,
|
|
9593
|
+
{
|
|
9594
|
+
method: setting ? "PATCH" : writing ? "POST" : "GET",
|
|
9595
|
+
headers: {
|
|
9596
|
+
Accept: "application/json",
|
|
9597
|
+
Authorization: `Bearer ${grant.accessToken}`,
|
|
9598
|
+
...body ? { "Content-Type": "application/json" } : {}
|
|
9599
|
+
},
|
|
9600
|
+
...body ? { body: JSON.stringify(body) } : {},
|
|
9601
|
+
signal: AbortSignal.timeout(3e4)
|
|
9602
|
+
}
|
|
9603
|
+
);
|
|
9604
|
+
if (action === "export" && response.ok) {
|
|
9605
|
+
if (!(response instanceof Response))
|
|
9606
|
+
throw new Error("The API transport does not support file exports.");
|
|
9607
|
+
const mime = values.format === "csv" ? "text/csv" : "application/json";
|
|
9608
|
+
const rows = Number(response.headers.get("X-SEO-Export-Rows"));
|
|
9609
|
+
if (response.headers.get("X-SEO-Audit-Id") !== values.audit || !response.headers.get("Content-Type")?.startsWith(mime) || !response.headers.has("X-SEO-Export-Rows") || !Number.isSafeInteger(rows) || rows < 0)
|
|
9610
|
+
throw new Error(
|
|
9611
|
+
"The export response does not match the requested audit."
|
|
9612
|
+
);
|
|
9613
|
+
const text = await response.text();
|
|
9614
|
+
if (values.format === "json")
|
|
9615
|
+
z16.object({
|
|
9616
|
+
contractVersion: z16.literal(1),
|
|
9617
|
+
audit: z16.object({
|
|
9618
|
+
id: z16.literal(values.audit),
|
|
9619
|
+
resourceId: z16.literal(context.resourceId)
|
|
9620
|
+
}),
|
|
9621
|
+
kind: z16.literal(values.kind),
|
|
9622
|
+
totalRows: z16.literal(rows),
|
|
9623
|
+
rows: z16.array(z16.unknown()).length(rows)
|
|
9624
|
+
}).parse(JSON.parse(text));
|
|
9625
|
+
const output = path29.resolve(options.cwd ?? process.cwd(), values.output);
|
|
9626
|
+
await writeFile8(output, text, { flag: "wx", mode: 384 });
|
|
9627
|
+
return {
|
|
9628
|
+
exitCode: 0,
|
|
9629
|
+
stdout: JSON.stringify(
|
|
9630
|
+
{
|
|
9631
|
+
auditId: values.audit,
|
|
9632
|
+
kind: values.kind,
|
|
9633
|
+
format: values.format,
|
|
9634
|
+
rows,
|
|
9635
|
+
output
|
|
9636
|
+
},
|
|
9637
|
+
null,
|
|
9638
|
+
2
|
|
9639
|
+
)
|
|
9640
|
+
};
|
|
9641
|
+
}
|
|
9642
|
+
const data = await response.json();
|
|
9643
|
+
if (!response.ok) {
|
|
9644
|
+
const result = z16.object({
|
|
9645
|
+
error: z16.object({ code: z16.string(), message: z16.string().max(500) })
|
|
9646
|
+
}).safeParse(data);
|
|
9647
|
+
throw new SiteOSAuthApiError({
|
|
9648
|
+
code: result.success ? result.data.error.code : "SEO_REQUEST_FAILED",
|
|
9649
|
+
message: result.success ? result.data.error.message : "The SEO request failed.",
|
|
9650
|
+
status: response.status
|
|
9651
|
+
});
|
|
9652
|
+
}
|
|
9653
|
+
const record = z16.object({ contractVersion: z16.literal(1) }).passthrough().parse(data);
|
|
9654
|
+
if (automation) {
|
|
9655
|
+
z16.literal(context.resourceId).parse(record.resourceId);
|
|
9656
|
+
const schedule = z16.object({
|
|
9657
|
+
enabled: z16.boolean(),
|
|
9658
|
+
weekday: z16.number().int().min(1).max(7),
|
|
9659
|
+
time: z16.string(),
|
|
9660
|
+
timeZone: z16.string(),
|
|
9661
|
+
revision: z16.number().int().min(0),
|
|
9662
|
+
nextRunAt: z16.string().nullable()
|
|
9663
|
+
});
|
|
9664
|
+
const notificationRoute = z16.object({
|
|
9665
|
+
enabled: z16.boolean(),
|
|
9666
|
+
minimumSeverity: z16.enum(["error", "warning"]),
|
|
9667
|
+
includeFailures: z16.boolean(),
|
|
9668
|
+
revision: z16.number().int().min(0),
|
|
9669
|
+
destinationId: z16.string().nullable()
|
|
9670
|
+
});
|
|
9671
|
+
if (retryNotification) {
|
|
9672
|
+
z16.literal(true).parse(record.retryQueued);
|
|
9673
|
+
z16.literal(positionals[2]).parse(record.notificationId);
|
|
9674
|
+
} else if (route === "notifications destinations")
|
|
9675
|
+
z16.object({
|
|
9676
|
+
candidates: z16.array(
|
|
9677
|
+
z16.object({
|
|
9678
|
+
candidateId: z16.string(),
|
|
9679
|
+
label: z16.string(),
|
|
9680
|
+
availability: z16.literal("available")
|
|
9681
|
+
})
|
|
9682
|
+
)
|
|
9683
|
+
}).parse(record);
|
|
9684
|
+
else if (setting) {
|
|
9685
|
+
const value = action === "schedule" ? schedule.parse(record.schedule) : notificationRoute.parse(record.route);
|
|
9686
|
+
if (value.revision !== Number(values.revision) + 1 || value.enabled !== (values.enabled === "true"))
|
|
9687
|
+
throw new Error("The saved settings do not match this change.");
|
|
9688
|
+
} else {
|
|
9689
|
+
schedule.parse(record.schedule);
|
|
9690
|
+
notificationRoute.parse(record.route);
|
|
9691
|
+
}
|
|
9692
|
+
} else if (!writing) {
|
|
9693
|
+
const validated = z16.object({
|
|
9694
|
+
resource: z16.object({
|
|
9695
|
+
id: z16.literal(context.resourceId),
|
|
9696
|
+
organizationId: z16.literal(context.overview.project.organizationId)
|
|
9697
|
+
}),
|
|
9698
|
+
audits: z16.array(z16.object({ id: z16.string() }).passthrough()),
|
|
9699
|
+
audit: z16.object({
|
|
9700
|
+
id: z16.string(),
|
|
9701
|
+
resourceId: z16.literal(context.resourceId)
|
|
9702
|
+
}).passthrough().nullable(),
|
|
9703
|
+
pages: z16.array(z16.unknown()),
|
|
9704
|
+
issues: z16.array(z16.unknown()),
|
|
9705
|
+
changes: z16.array(z16.unknown()),
|
|
9706
|
+
totalChanges: z16.number(),
|
|
9707
|
+
dispositions: z16.array(z16.unknown())
|
|
9708
|
+
}).passthrough().parse(record);
|
|
9709
|
+
const selected = query.get("audit");
|
|
9710
|
+
if (selected && validated.audit?.id !== selected)
|
|
9711
|
+
throw new Error("The SEO response does not match the requested audit.");
|
|
9712
|
+
} else if (record.audit)
|
|
9713
|
+
z16.object({
|
|
9714
|
+
id: z16.string(),
|
|
9715
|
+
resourceId: z16.literal(context.resourceId),
|
|
9716
|
+
organizationId: z16.literal(context.overview.project.organizationId),
|
|
9717
|
+
state: z16.literal("queued")
|
|
9718
|
+
}).parse(record.audit);
|
|
9719
|
+
else if (route === "audit cancel") z16.literal(true).parse(record.cancelled);
|
|
9720
|
+
else if (action === "issue")
|
|
9721
|
+
z16.object({
|
|
9722
|
+
url: z16.literal(values.url),
|
|
9723
|
+
ruleId: z16.literal(values.rule),
|
|
9724
|
+
ignored: z16.literal(positionals[1] === "ignore"),
|
|
9725
|
+
revision: z16.literal(Number(values.revision) + 1)
|
|
9726
|
+
}).parse(record.disposition);
|
|
9727
|
+
else throw new Error("The SEO service returned an invalid response.");
|
|
9728
|
+
return { exitCode: 0, stdout: JSON.stringify(record, null, 2) };
|
|
9729
|
+
} catch (cause) {
|
|
9730
|
+
const error = {
|
|
9731
|
+
code: cause instanceof SiteOSAuthApiError ? cause.code : "SEO_COMMAND_FAILED",
|
|
9732
|
+
message: cause instanceof z16.ZodError ? "The SEO service returned an invalid response." : cause instanceof Error ? cause.message : "The SEO command failed."
|
|
9733
|
+
};
|
|
9734
|
+
return {
|
|
9735
|
+
exitCode: cause instanceof SiteOSAuthApiError ? 1 : 2,
|
|
9736
|
+
...json ? { stdout: JSON.stringify({ error }) } : { stderr: error.message }
|
|
8863
9737
|
};
|
|
8864
9738
|
}
|
|
8865
9739
|
}
|
|
@@ -8897,6 +9771,12 @@ if (!command || command === "--help" || command === "-h") {
|
|
|
8897
9771
|
}
|
|
8898
9772
|
function rootCommandRegistry() {
|
|
8899
9773
|
return createCommandRegistry({
|
|
9774
|
+
seo: (args2) => runSeoCommand({
|
|
9775
|
+
args: args2,
|
|
9776
|
+
cwd: process.cwd(),
|
|
9777
|
+
env: process.env,
|
|
9778
|
+
fetchImpl: globalThis.fetch
|
|
9779
|
+
}),
|
|
8900
9780
|
"health-check": runHealthCheck,
|
|
8901
9781
|
project: (args2) => runProjectCommand4({
|
|
8902
9782
|
args: args2,
|
|
@@ -9004,6 +9884,7 @@ Usage:
|
|
|
9004
9884
|
siteos project --help
|
|
9005
9885
|
siteos cookie --help
|
|
9006
9886
|
siteos trace --help
|
|
9887
|
+
siteos seo --help
|
|
9007
9888
|
siteos integrations --help
|
|
9008
9889
|
siteos pulse --help
|
|
9009
9890
|
siteos search --help
|
|
@@ -9015,6 +9896,7 @@ Commands:
|
|
|
9015
9896
|
project Select one Project and configure its services and environments.
|
|
9016
9897
|
cookie Configure, publish, and inspect the Project\u2019s cookie banner.
|
|
9017
9898
|
trace Configure analytics observation and inspect evidence.
|
|
9899
|
+
seo Audit HTML, inspect changes and verify fixes.
|
|
9018
9900
|
integrations Manage Organization connections and destinations.
|
|
9019
9901
|
pulse Manage monitoring checks, tests, and deployments.
|
|
9020
9902
|
search Run SiteOS search operations for a project environment.
|