@riddledc/riddle-proof 0.7.97 → 0.7.99
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-G3UY3SHZ.js → chunk-W5PBTTMJ.js} +332 -14
- package/dist/cli.cjs +361 -14
- package/dist/cli.js +30 -1
- package/dist/index.cjs +332 -14
- package/dist/index.js +1 -1
- package/dist/profile.cjs +332 -14
- package/dist/profile.d.cts +8 -1
- package/dist/profile.d.ts +8 -1
- package/dist/profile.js +1 -1
- package/package.json +1 -1
|
@@ -29,6 +29,7 @@ var RIDDLE_PROOF_PROFILE_CHECK_TYPES = [
|
|
|
29
29
|
"frame_no_horizontal_overflow",
|
|
30
30
|
"text_visible",
|
|
31
31
|
"text_absent",
|
|
32
|
+
"http_status",
|
|
32
33
|
"link_status",
|
|
33
34
|
"artifact_link_status",
|
|
34
35
|
"route_inventory",
|
|
@@ -778,19 +779,34 @@ function normalizeCheck(input, index) {
|
|
|
778
779
|
if (type === "route_inventory" && !expectedRoutes?.length) {
|
|
779
780
|
throw new Error(`checks[${index}] route_inventory requires expected_routes.`);
|
|
780
781
|
}
|
|
782
|
+
const isHttpStatusCheck = type === "http_status";
|
|
781
783
|
const isLinkStatusCheck = type === "link_status" || type === "artifact_link_status";
|
|
782
|
-
const
|
|
783
|
-
const
|
|
784
|
+
const isStatusCheck = isHttpStatusCheck || isLinkStatusCheck;
|
|
785
|
+
const requestUrl = stringFromOwn(input, "url", "endpoint_url", "endpointUrl", "endpoint", "request_url", "requestUrl", "path");
|
|
786
|
+
if (isHttpStatusCheck && !requestUrl) {
|
|
787
|
+
throw new Error(`checks[${index}] http_status requires url.`);
|
|
788
|
+
}
|
|
789
|
+
const method = stringFromOwn(input, "method", "http_method", "httpMethod")?.toUpperCase();
|
|
790
|
+
if (isHttpStatusCheck && method && !/^[A-Z]+$/.test(method)) {
|
|
791
|
+
throw new Error(`checks[${index}] http_status method must contain only letters.`);
|
|
792
|
+
}
|
|
793
|
+
const hasBodyJson = hasOwn(input, "body_json") || hasOwn(input, "bodyJson") || hasOwn(input, "json");
|
|
794
|
+
const expectedStatus = isStatusCheck ? normalizeHttpStatus(input.expected_status ?? input.expectedStatus ?? input.status, `checks[${index}] expected_status`) : void 0;
|
|
795
|
+
const allowedStatuses = isStatusCheck ? normalizeHttpStatuses(
|
|
784
796
|
input.allowed_statuses ?? input.allowedStatuses ?? input.expected_statuses ?? input.expectedStatuses,
|
|
785
797
|
`checks[${index}] allowed_statuses`
|
|
786
798
|
) : void 0;
|
|
787
799
|
const maxLinks = isLinkStatusCheck ? normalizePositiveInteger(input.max_links ?? input.maxLinks ?? input.limit, `checks[${index}] max_links`, 500) : void 0;
|
|
788
|
-
const minBytes =
|
|
789
|
-
const expectedContentType =
|
|
790
|
-
const allowedContentTypes =
|
|
800
|
+
const minBytes = isStatusCheck ? normalizePositiveInteger(input.min_bytes ?? input.minBytes ?? input.min_response_bytes ?? input.minResponseBytes, `checks[${index}] min_bytes`) : void 0;
|
|
801
|
+
const expectedContentType = isStatusCheck ? stringValue(input.expected_content_type) || stringValue(input.expectedContentType) || stringValue(input.content_type) || stringValue(input.contentType) : void 0;
|
|
802
|
+
const allowedContentTypes = isStatusCheck ? normalizeStringList(
|
|
791
803
|
input.allowed_content_types ?? input.allowedContentTypes ?? input.expected_content_types ?? input.expectedContentTypes,
|
|
792
804
|
`checks[${index}] allowed_content_types`
|
|
793
805
|
) ?? (expectedContentType ? [expectedContentType] : void 0) : void 0;
|
|
806
|
+
const bodyContains = isHttpStatusCheck ? normalizeStringList(
|
|
807
|
+
input.body_contains ?? input.bodyContains ?? input.expected_body_contains ?? input.expectedBodyContains ?? input.response_body_contains ?? input.responseBodyContains ?? input.body_includes ?? input.bodyIncludes,
|
|
808
|
+
`checks[${index}] body_contains`
|
|
809
|
+
) : void 0;
|
|
794
810
|
if (isLinkStatusCheck) {
|
|
795
811
|
if (minCount !== void 0 && (!Number.isInteger(minCount) || minCount < 0)) {
|
|
796
812
|
throw new Error(`checks[${index}] ${type} min_count must be a non-negative integer.`);
|
|
@@ -808,6 +824,12 @@ function normalizeCheck(input, index) {
|
|
|
808
824
|
expected_url: expectedUrl,
|
|
809
825
|
expected_routes: expectedRoutes,
|
|
810
826
|
selector: stringValue(input.selector),
|
|
827
|
+
url: isHttpStatusCheck ? requestUrl : void 0,
|
|
828
|
+
method: isHttpStatusCheck ? method || "GET" : void 0,
|
|
829
|
+
headers: isHttpStatusCheck ? stringRecord(input.headers) : void 0,
|
|
830
|
+
body: isHttpStatusCheck ? stringValue(input.body) : void 0,
|
|
831
|
+
body_json: isHttpStatusCheck && hasBodyJson ? toJsonValue(input.body_json ?? input.bodyJson ?? input.json) : void 0,
|
|
832
|
+
body_contains: bodyContains,
|
|
811
833
|
expected_texts: expectedTexts,
|
|
812
834
|
link_selector: stringValue(input.link_selector) || stringValue(input.linkSelector),
|
|
813
835
|
source_selector: stringValue(input.source_selector) || stringValue(input.sourceSelector),
|
|
@@ -831,7 +853,7 @@ function normalizeCheck(input, index) {
|
|
|
831
853
|
max_links: maxLinks,
|
|
832
854
|
same_origin_only: isLinkStatusCheck ? input.same_origin_only === true || input.sameOriginOnly === true : void 0,
|
|
833
855
|
dedupe: isLinkStatusCheck ? input.dedupe === false ? false : true : void 0,
|
|
834
|
-
require_nonzero_bytes:
|
|
856
|
+
require_nonzero_bytes: isStatusCheck ? input.require_nonzero_bytes === true || input.requireNonzeroBytes === true : void 0,
|
|
835
857
|
min_bytes: minBytes,
|
|
836
858
|
allowed_content_types: allowedContentTypes,
|
|
837
859
|
allow_get_fallback: isLinkStatusCheck ? input.allow_get_fallback === false || input.allowGetFallback === false ? false : true : void 0,
|
|
@@ -930,14 +952,32 @@ function selectorKey(check) {
|
|
|
930
952
|
function linkStatusSelector(check) {
|
|
931
953
|
return check.selector || check.link_selector || "a[href]";
|
|
932
954
|
}
|
|
933
|
-
function
|
|
955
|
+
function httpStatusRequestUrl(check, baseUrl) {
|
|
956
|
+
const rawUrl = check.url || "";
|
|
957
|
+
if (!rawUrl) return "";
|
|
958
|
+
try {
|
|
959
|
+
return baseUrl ? new URL(rawUrl, baseUrl).href : new URL(rawUrl).href;
|
|
960
|
+
} catch {
|
|
961
|
+
return rawUrl;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
function httpStatusMethod(check) {
|
|
965
|
+
return (check.method || "GET").toUpperCase();
|
|
966
|
+
}
|
|
967
|
+
function httpStatusKey(check, baseUrl) {
|
|
968
|
+
return `${httpStatusMethod(check)} ${httpStatusRequestUrl(check, baseUrl)}`;
|
|
969
|
+
}
|
|
970
|
+
function httpStatusAllowedStatuses(check) {
|
|
934
971
|
if (check.allowed_statuses?.length) return check.allowed_statuses;
|
|
935
972
|
if (check.expected_status !== void 0) return [check.expected_status];
|
|
936
973
|
return void 0;
|
|
937
974
|
}
|
|
938
|
-
function
|
|
975
|
+
function linkStatusAllowedStatuses(check) {
|
|
976
|
+
return httpStatusAllowedStatuses(check);
|
|
977
|
+
}
|
|
978
|
+
function httpStatusIsAllowed(status, check) {
|
|
939
979
|
if (status === void 0) return false;
|
|
940
|
-
const allowed =
|
|
980
|
+
const allowed = httpStatusAllowedStatuses(check);
|
|
941
981
|
return allowed?.length ? allowed.includes(status) : status >= 200 && status < 400;
|
|
942
982
|
}
|
|
943
983
|
function linkStatusObservedBytes(result) {
|
|
@@ -961,9 +1001,15 @@ function linkStatusContentTypeOk(result, check) {
|
|
|
961
1001
|
return actual === normalized;
|
|
962
1002
|
});
|
|
963
1003
|
}
|
|
1004
|
+
function httpStatusBodyContainsFailures(result, check) {
|
|
1005
|
+
const expected = check.body_contains?.filter(Boolean) ?? [];
|
|
1006
|
+
if (!expected.length) return [];
|
|
1007
|
+
const observed = isRecord(result.body_contains) ? result.body_contains : {};
|
|
1008
|
+
return expected.filter((text) => observed[text] !== true);
|
|
1009
|
+
}
|
|
964
1010
|
function linkStatusResultOk(result, check) {
|
|
965
1011
|
const status = numberValue(result.status);
|
|
966
|
-
if (!
|
|
1012
|
+
if (!httpStatusIsAllowed(status, check)) return false;
|
|
967
1013
|
if (stringValue(result.error)) return false;
|
|
968
1014
|
if (result.ok === false) return false;
|
|
969
1015
|
if (!linkStatusContentTypeOk(result, check)) return false;
|
|
@@ -975,8 +1021,64 @@ function linkStatusResultOk(result, check) {
|
|
|
975
1021
|
const observedBytes = linkStatusObservedBytes(result);
|
|
976
1022
|
if (observedBytes === void 0 || observedBytes < check.min_bytes) return false;
|
|
977
1023
|
}
|
|
1024
|
+
if (httpStatusBodyContainsFailures(result, check).length) return false;
|
|
978
1025
|
return true;
|
|
979
1026
|
}
|
|
1027
|
+
function httpStatusEvidenceForCheck(viewport, check) {
|
|
1028
|
+
const evidence = viewport.http_statuses?.[httpStatusKey(check, viewport.url)];
|
|
1029
|
+
return isRecord(evidence) ? evidence : void 0;
|
|
1030
|
+
}
|
|
1031
|
+
function summarizeHttpStatusEvidence(viewport, check) {
|
|
1032
|
+
const key = httpStatusKey(check, viewport.url);
|
|
1033
|
+
const statusEvidence = httpStatusEvidenceForCheck(viewport, check);
|
|
1034
|
+
if (!statusEvidence) {
|
|
1035
|
+
return {
|
|
1036
|
+
viewport: viewport.name,
|
|
1037
|
+
key,
|
|
1038
|
+
url: httpStatusRequestUrl(check, viewport.url),
|
|
1039
|
+
method: httpStatusMethod(check),
|
|
1040
|
+
ok: false,
|
|
1041
|
+
status: null,
|
|
1042
|
+
failures: [{ code: "http_status_evidence_missing" }]
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
const failures = [];
|
|
1046
|
+
const bodyContainsMissing = httpStatusBodyContainsFailures(statusEvidence, check);
|
|
1047
|
+
if (!linkStatusResultOk(statusEvidence, check)) {
|
|
1048
|
+
failures.push({
|
|
1049
|
+
code: "http_status_failed",
|
|
1050
|
+
url: stringValue(statusEvidence.url) ?? httpStatusRequestUrl(check, viewport.url),
|
|
1051
|
+
status: numberValue(statusEvidence.status) ?? null,
|
|
1052
|
+
method: stringValue(statusEvidence.method) ?? httpStatusMethod(check),
|
|
1053
|
+
error: stringValue(statusEvidence.error) ?? null,
|
|
1054
|
+
content_type: stringValue(statusEvidence.content_type) ?? null,
|
|
1055
|
+
bytes: linkStatusObservedBytes(statusEvidence) ?? null,
|
|
1056
|
+
allowed_statuses: httpStatusAllowedStatuses(check) ?? ["2xx", "3xx"],
|
|
1057
|
+
min_bytes: check.min_bytes ?? null,
|
|
1058
|
+
allowed_content_types: check.allowed_content_types ?? null,
|
|
1059
|
+
body_contains: check.body_contains ?? null,
|
|
1060
|
+
body_contains_missing: bodyContainsMissing,
|
|
1061
|
+
body_sample: stringValue(statusEvidence.body_sample) ?? null
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
return {
|
|
1065
|
+
viewport: viewport.name,
|
|
1066
|
+
key,
|
|
1067
|
+
url: stringValue(statusEvidence.url) ?? httpStatusRequestUrl(check, viewport.url),
|
|
1068
|
+
method: stringValue(statusEvidence.method) ?? httpStatusMethod(check),
|
|
1069
|
+
status: numberValue(statusEvidence.status) ?? null,
|
|
1070
|
+
status_text: stringValue(statusEvidence.status_text) ?? null,
|
|
1071
|
+
ok: failures.length === 0,
|
|
1072
|
+
error: stringValue(statusEvidence.error) ?? null,
|
|
1073
|
+
content_type: stringValue(statusEvidence.content_type) ?? null,
|
|
1074
|
+
content_length: numberValue(statusEvidence.content_length) ?? null,
|
|
1075
|
+
bytes: linkStatusObservedBytes(statusEvidence) ?? null,
|
|
1076
|
+
body_contains: isRecord(statusEvidence.body_contains) ? toJsonValue(statusEvidence.body_contains) : null,
|
|
1077
|
+
body_contains_missing: bodyContainsMissing,
|
|
1078
|
+
body_sample: stringValue(statusEvidence.body_sample) ?? null,
|
|
1079
|
+
failures
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
980
1082
|
function linkStatusEvidenceForCheck(viewport, check) {
|
|
981
1083
|
const evidence = viewport.link_statuses?.[linkStatusSelector(check)];
|
|
982
1084
|
return isRecord(evidence) ? evidence : void 0;
|
|
@@ -1567,6 +1669,29 @@ function assessCheckFromEvidence(check, evidence) {
|
|
|
1567
1669
|
message: failed ? `Text assertion failed in ${failed} viewport(s).` : void 0
|
|
1568
1670
|
};
|
|
1569
1671
|
}
|
|
1672
|
+
if (check.type === "http_status") {
|
|
1673
|
+
const url = httpStatusRequestUrl(check, evidence.target_url);
|
|
1674
|
+
const method = httpStatusMethod(check);
|
|
1675
|
+
const summaries = viewports.map((viewport) => summarizeHttpStatusEvidence(viewport, check));
|
|
1676
|
+
const failed = summaries.filter((summary) => Array.isArray(summary.failures) && summary.failures.length > 0);
|
|
1677
|
+
return {
|
|
1678
|
+
type: check.type,
|
|
1679
|
+
label: checkLabel(check),
|
|
1680
|
+
status: failed.length ? "failed" : "passed",
|
|
1681
|
+
evidence: {
|
|
1682
|
+
url,
|
|
1683
|
+
method,
|
|
1684
|
+
allowed_statuses: httpStatusAllowedStatuses(check) || ["2xx", "3xx"],
|
|
1685
|
+
require_nonzero_bytes: check.require_nonzero_bytes === true,
|
|
1686
|
+
min_bytes: check.min_bytes ?? null,
|
|
1687
|
+
allowed_content_types: check.allowed_content_types ?? null,
|
|
1688
|
+
body_contains: check.body_contains ?? [],
|
|
1689
|
+
viewports: summaries.map((summary) => toJsonValue(summary)),
|
|
1690
|
+
failures: failed.flatMap((summary) => Array.isArray(summary.failures) ? summary.failures.map((failure) => toJsonValue({ viewport: stringValue(summary.viewport) ?? null, failure })) : [])
|
|
1691
|
+
},
|
|
1692
|
+
message: failed.length ? `HTTP status failed in ${failed.length} viewport(s).` : void 0
|
|
1693
|
+
};
|
|
1694
|
+
}
|
|
1570
1695
|
if (check.type === "link_status" || check.type === "artifact_link_status") {
|
|
1571
1696
|
const selector = linkStatusSelector(check);
|
|
1572
1697
|
const summaries = viewports.map((viewport) => summarizeLinkStatusEvidence(viewport, check));
|
|
@@ -2176,16 +2301,37 @@ function textOrderMatch(texts, expectedTexts) {
|
|
|
2176
2301
|
function linkStatusSelector(check) {
|
|
2177
2302
|
return check.selector || check.link_selector || "a[href]";
|
|
2178
2303
|
}
|
|
2179
|
-
function
|
|
2304
|
+
function httpStatusRequestUrl(check, baseUrl) {
|
|
2305
|
+
const rawUrl = check.url || "";
|
|
2306
|
+
if (!rawUrl) return "";
|
|
2307
|
+
try {
|
|
2308
|
+
return baseUrl ? new URL(rawUrl, baseUrl).href : new URL(rawUrl).href;
|
|
2309
|
+
} catch {
|
|
2310
|
+
return rawUrl;
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
function httpStatusMethod(check) {
|
|
2314
|
+
return String(check.method || "GET").toUpperCase();
|
|
2315
|
+
}
|
|
2316
|
+
function httpStatusKey(check, baseUrl) {
|
|
2317
|
+
return httpStatusMethod(check) + " " + httpStatusRequestUrl(check, baseUrl);
|
|
2318
|
+
}
|
|
2319
|
+
function httpStatusAllowedStatuses(check) {
|
|
2180
2320
|
if (Array.isArray(check.allowed_statuses) && check.allowed_statuses.length) return check.allowed_statuses;
|
|
2181
2321
|
if (typeof check.expected_status === "number" && Number.isFinite(check.expected_status)) return [check.expected_status];
|
|
2182
2322
|
return undefined;
|
|
2183
2323
|
}
|
|
2184
|
-
function
|
|
2324
|
+
function linkStatusAllowedStatuses(check) {
|
|
2325
|
+
return httpStatusAllowedStatuses(check);
|
|
2326
|
+
}
|
|
2327
|
+
function httpStatusIsAllowed(status, check) {
|
|
2185
2328
|
if (typeof status !== "number" || !Number.isFinite(status)) return false;
|
|
2186
|
-
const allowed =
|
|
2329
|
+
const allowed = httpStatusAllowedStatuses(check);
|
|
2187
2330
|
return Array.isArray(allowed) && allowed.length ? allowed.includes(status) : status >= 200 && status < 400;
|
|
2188
2331
|
}
|
|
2332
|
+
function linkStatusIsAllowed(status, check) {
|
|
2333
|
+
return httpStatusIsAllowed(status, check);
|
|
2334
|
+
}
|
|
2189
2335
|
function linkStatusObservedBytes(result) {
|
|
2190
2336
|
const bytes = typeof result.bytes === "number" && Number.isFinite(result.bytes) ? result.bytes : undefined;
|
|
2191
2337
|
const contentLength = typeof result.content_length === "number" && Number.isFinite(result.content_length) ? result.content_length : undefined;
|
|
@@ -2208,9 +2354,17 @@ function linkStatusContentTypeOk(result, check) {
|
|
|
2208
2354
|
return actual === normalized;
|
|
2209
2355
|
});
|
|
2210
2356
|
}
|
|
2357
|
+
function httpStatusBodyContainsFailures(result, check) {
|
|
2358
|
+
const expected = Array.isArray(check.body_contains) ? check.body_contains.filter(Boolean) : [];
|
|
2359
|
+
if (!expected.length) return [];
|
|
2360
|
+
const observed = result && typeof result.body_contains === "object" && !Array.isArray(result.body_contains)
|
|
2361
|
+
? result.body_contains
|
|
2362
|
+
: {};
|
|
2363
|
+
return expected.filter((text) => observed[text] !== true);
|
|
2364
|
+
}
|
|
2211
2365
|
function linkStatusResultOk(result, check) {
|
|
2212
2366
|
if (!result || typeof result !== "object" || Array.isArray(result)) return false;
|
|
2213
|
-
if (!
|
|
2367
|
+
if (!httpStatusIsAllowed(result.status, check)) return false;
|
|
2214
2368
|
if (typeof result.error === "string" && result.error.trim()) return false;
|
|
2215
2369
|
if (result.ok === false) return false;
|
|
2216
2370
|
if (!linkStatusContentTypeOk(result, check)) return false;
|
|
@@ -2222,8 +2376,62 @@ function linkStatusResultOk(result, check) {
|
|
|
2222
2376
|
const observedBytes = linkStatusObservedBytes(result);
|
|
2223
2377
|
if (observedBytes === undefined || observedBytes < check.min_bytes) return false;
|
|
2224
2378
|
}
|
|
2379
|
+
if (httpStatusBodyContainsFailures(result, check).length) return false;
|
|
2225
2380
|
return true;
|
|
2226
2381
|
}
|
|
2382
|
+
function summarizeHttpStatusEvidence(viewport, check) {
|
|
2383
|
+
const key = httpStatusKey(check, viewport && viewport.url);
|
|
2384
|
+
const statusEvidence = viewport && viewport.http_statuses && viewport.http_statuses[key];
|
|
2385
|
+
if (!statusEvidence || typeof statusEvidence !== "object" || Array.isArray(statusEvidence)) {
|
|
2386
|
+
return {
|
|
2387
|
+
viewport: viewport && viewport.name,
|
|
2388
|
+
key,
|
|
2389
|
+
url: httpStatusRequestUrl(check, viewport && viewport.url),
|
|
2390
|
+
method: httpStatusMethod(check),
|
|
2391
|
+
ok: false,
|
|
2392
|
+
status: null,
|
|
2393
|
+
failures: [{ code: "http_status_evidence_missing" }],
|
|
2394
|
+
};
|
|
2395
|
+
}
|
|
2396
|
+
const failures = [];
|
|
2397
|
+
const bodyContainsMissing = httpStatusBodyContainsFailures(statusEvidence, check);
|
|
2398
|
+
if (!linkStatusResultOk(statusEvidence, check)) {
|
|
2399
|
+
failures.push({
|
|
2400
|
+
code: "http_status_failed",
|
|
2401
|
+
url: typeof statusEvidence.url === "string" ? statusEvidence.url : httpStatusRequestUrl(check, viewport && viewport.url),
|
|
2402
|
+
status: typeof statusEvidence.status === "number" && Number.isFinite(statusEvidence.status) ? statusEvidence.status : null,
|
|
2403
|
+
method: typeof statusEvidence.method === "string" ? statusEvidence.method : httpStatusMethod(check),
|
|
2404
|
+
error: typeof statusEvidence.error === "string" ? statusEvidence.error : null,
|
|
2405
|
+
content_type: typeof statusEvidence.content_type === "string" ? statusEvidence.content_type : null,
|
|
2406
|
+
bytes: linkStatusObservedBytes(statusEvidence) ?? null,
|
|
2407
|
+
allowed_statuses: httpStatusAllowedStatuses(check) || ["2xx", "3xx"],
|
|
2408
|
+
min_bytes: typeof check.min_bytes === "number" && Number.isFinite(check.min_bytes) ? check.min_bytes : null,
|
|
2409
|
+
allowed_content_types: Array.isArray(check.allowed_content_types) ? check.allowed_content_types : null,
|
|
2410
|
+
body_contains: Array.isArray(check.body_contains) ? check.body_contains : null,
|
|
2411
|
+
body_contains_missing: bodyContainsMissing,
|
|
2412
|
+
body_sample: typeof statusEvidence.body_sample === "string" ? statusEvidence.body_sample : null,
|
|
2413
|
+
});
|
|
2414
|
+
}
|
|
2415
|
+
return {
|
|
2416
|
+
viewport: viewport && viewport.name,
|
|
2417
|
+
key,
|
|
2418
|
+
url: typeof statusEvidence.url === "string" ? statusEvidence.url : httpStatusRequestUrl(check, viewport && viewport.url),
|
|
2419
|
+
method: typeof statusEvidence.method === "string" ? statusEvidence.method : httpStatusMethod(check),
|
|
2420
|
+
status: typeof statusEvidence.status === "number" && Number.isFinite(statusEvidence.status) ? statusEvidence.status : null,
|
|
2421
|
+
status_text: typeof statusEvidence.status_text === "string" ? statusEvidence.status_text : null,
|
|
2422
|
+
ok: failures.length === 0,
|
|
2423
|
+
error: typeof statusEvidence.error === "string" ? statusEvidence.error : null,
|
|
2424
|
+
content_type: typeof statusEvidence.content_type === "string" ? statusEvidence.content_type : null,
|
|
2425
|
+
content_length: typeof statusEvidence.content_length === "number" && Number.isFinite(statusEvidence.content_length) ? statusEvidence.content_length : null,
|
|
2426
|
+
bytes: linkStatusObservedBytes(statusEvidence) ?? null,
|
|
2427
|
+
body_contains: statusEvidence.body_contains && typeof statusEvidence.body_contains === "object" && !Array.isArray(statusEvidence.body_contains)
|
|
2428
|
+
? statusEvidence.body_contains
|
|
2429
|
+
: null,
|
|
2430
|
+
body_contains_missing: bodyContainsMissing,
|
|
2431
|
+
body_sample: typeof statusEvidence.body_sample === "string" ? statusEvidence.body_sample : null,
|
|
2432
|
+
failures,
|
|
2433
|
+
};
|
|
2434
|
+
}
|
|
2227
2435
|
function summarizeLinkStatusEvidence(viewport, check) {
|
|
2228
2436
|
const selector = linkStatusSelector(check);
|
|
2229
2437
|
const linkEvidence = viewport && viewport.link_statuses && viewport.link_statuses[selector];
|
|
@@ -2979,6 +3187,31 @@ function assessProfile(profile, evidence) {
|
|
|
2979
3187
|
});
|
|
2980
3188
|
continue;
|
|
2981
3189
|
}
|
|
3190
|
+
if (check.type === "http_status") {
|
|
3191
|
+
const url = httpStatusRequestUrl(check, evidence.target_url);
|
|
3192
|
+
const method = httpStatusMethod(check);
|
|
3193
|
+
const summaries = checkViewports.map((viewport) => summarizeHttpStatusEvidence(viewport, check));
|
|
3194
|
+
const failed = summaries.filter((summary) => Array.isArray(summary.failures) && summary.failures.length > 0);
|
|
3195
|
+
checks.push({
|
|
3196
|
+
type: check.type,
|
|
3197
|
+
label: check.label || check.type,
|
|
3198
|
+
status: failed.length ? "failed" : "passed",
|
|
3199
|
+
evidence: {
|
|
3200
|
+
url,
|
|
3201
|
+
method,
|
|
3202
|
+
allowed_statuses: httpStatusAllowedStatuses(check) || ["2xx", "3xx"],
|
|
3203
|
+
require_nonzero_bytes: check.require_nonzero_bytes === true,
|
|
3204
|
+
min_bytes: check.min_bytes ?? null,
|
|
3205
|
+
allowed_content_types: check.allowed_content_types ?? null,
|
|
3206
|
+
viewports: summaries,
|
|
3207
|
+
failures: failed.flatMap((summary) => Array.isArray(summary.failures)
|
|
3208
|
+
? summary.failures.map((failure) => ({ viewport: summary.viewport || null, failure }))
|
|
3209
|
+
: []),
|
|
3210
|
+
},
|
|
3211
|
+
message: failed.length ? "HTTP status failed in " + failed.length + " viewport(s)." : undefined,
|
|
3212
|
+
});
|
|
3213
|
+
continue;
|
|
3214
|
+
}
|
|
2982
3215
|
if (check.type === "link_status" || check.type === "artifact_link_status") {
|
|
2983
3216
|
const selector = linkStatusSelector(check);
|
|
2984
3217
|
const summaries = checkViewports.map((viewport) => summarizeLinkStatusEvidence(viewport, check));
|
|
@@ -4176,6 +4409,72 @@ function linkProbeResponseFields(response, method) {
|
|
|
4176
4409
|
content_length: contentLength,
|
|
4177
4410
|
};
|
|
4178
4411
|
}
|
|
4412
|
+
async function collectHttpStatus(check) {
|
|
4413
|
+
const url = httpStatusRequestUrl(check, page.url() || targetUrl);
|
|
4414
|
+
const method = httpStatusMethod(check);
|
|
4415
|
+
const headers = check.headers && typeof check.headers === "object" && !Array.isArray(check.headers)
|
|
4416
|
+
? Object.fromEntries(Object.entries(check.headers).map(([key, value]) => [key, String(value)]).filter(([key]) => key.trim()))
|
|
4417
|
+
: {};
|
|
4418
|
+
let body;
|
|
4419
|
+
if (check.body_json !== undefined) {
|
|
4420
|
+
body = JSON.stringify(check.body_json);
|
|
4421
|
+
if (!Object.keys(headers).some((key) => key.toLowerCase() === "content-type")) headers["content-type"] = "application/json";
|
|
4422
|
+
} else if (typeof check.body === "string") {
|
|
4423
|
+
body = check.body;
|
|
4424
|
+
}
|
|
4425
|
+
const bodyContains = Array.isArray(check.body_contains) ? check.body_contains.filter(Boolean) : [];
|
|
4426
|
+
const options = {
|
|
4427
|
+
method,
|
|
4428
|
+
redirect: "follow",
|
|
4429
|
+
cache: "no-store",
|
|
4430
|
+
headers,
|
|
4431
|
+
};
|
|
4432
|
+
if (body !== undefined && method !== "GET" && method !== "HEAD") options.body = body;
|
|
4433
|
+
const result = {
|
|
4434
|
+
version: "riddle-proof.http-status.v1",
|
|
4435
|
+
url,
|
|
4436
|
+
method,
|
|
4437
|
+
status: null,
|
|
4438
|
+
ok: false,
|
|
4439
|
+
error: null,
|
|
4440
|
+
request_body_bytes: typeof body === "string" ? body.length : 0,
|
|
4441
|
+
allowed_statuses: httpStatusAllowedStatuses(check) || ["2xx", "3xx"],
|
|
4442
|
+
require_nonzero_bytes: check.require_nonzero_bytes === true,
|
|
4443
|
+
min_bytes: typeof check.min_bytes === "number" && Number.isFinite(check.min_bytes) ? check.min_bytes : null,
|
|
4444
|
+
allowed_content_types: Array.isArray(check.allowed_content_types) ? check.allowed_content_types : null,
|
|
4445
|
+
};
|
|
4446
|
+
try {
|
|
4447
|
+
const response = await fetch(url, options);
|
|
4448
|
+
Object.assign(result, linkProbeResponseFields(response, method));
|
|
4449
|
+
result.url = url;
|
|
4450
|
+
result.status_text = response.statusText || "";
|
|
4451
|
+
const shouldReadBody = check.require_nonzero_bytes === true || (typeof check.min_bytes === "number" && Number.isFinite(check.min_bytes)) || bodyContains.length > 0;
|
|
4452
|
+
if (shouldReadBody) {
|
|
4453
|
+
try {
|
|
4454
|
+
const buffer = await response.arrayBuffer();
|
|
4455
|
+
result.bytes = buffer.byteLength;
|
|
4456
|
+
if (bodyContains.length) {
|
|
4457
|
+
const text = new TextDecoder().decode(buffer);
|
|
4458
|
+
result.body_sample = text.slice(0, 1000);
|
|
4459
|
+
result.body_contains = Object.fromEntries(bodyContains.map((expected) => [expected, text.includes(expected)]));
|
|
4460
|
+
}
|
|
4461
|
+
} catch (error) {
|
|
4462
|
+
result.error = String(error && error.message ? error.message : error).slice(0, 500);
|
|
4463
|
+
}
|
|
4464
|
+
}
|
|
4465
|
+
result.ok = linkProbeAllowed(result.status, check)
|
|
4466
|
+
&& linkProbeContentTypeAllowed(result, check)
|
|
4467
|
+
&& (check.require_nonzero_bytes !== true || ((linkProbeObservedBytes(result) || 0) > 0))
|
|
4468
|
+
&& (!(typeof check.min_bytes === "number" && Number.isFinite(check.min_bytes)) || ((linkProbeObservedBytes(result) || 0) >= check.min_bytes))
|
|
4469
|
+
&& (!bodyContains.length || bodyContains.every((expected) => result.body_contains && result.body_contains[expected] === true))
|
|
4470
|
+
&& !result.error;
|
|
4471
|
+
return result;
|
|
4472
|
+
} catch (error) {
|
|
4473
|
+
result.error = String(error && error.message ? error.message : error).slice(0, 500);
|
|
4474
|
+
result.ok = false;
|
|
4475
|
+
return result;
|
|
4476
|
+
}
|
|
4477
|
+
}
|
|
4179
4478
|
async function probeLinkStatus(candidate, check) {
|
|
4180
4479
|
const requireNonzeroBytes = check.require_nonzero_bytes === true;
|
|
4181
4480
|
const minBytes = typeof check.min_bytes === "number" && Number.isFinite(check.min_bytes) ? Math.max(1, Math.floor(check.min_bytes)) : null;
|
|
@@ -4868,6 +5167,7 @@ async function captureViewport(viewport) {
|
|
|
4868
5167
|
const frames = {};
|
|
4869
5168
|
const text_sequences = {};
|
|
4870
5169
|
const text_matches = {};
|
|
5170
|
+
const http_statuses = {};
|
|
4871
5171
|
const link_statuses = {};
|
|
4872
5172
|
for (const check of profile.checks || []) {
|
|
4873
5173
|
if (!profileCheckAppliesToViewport(check, viewport)) continue;
|
|
@@ -4894,6 +5194,10 @@ async function captureViewport(viewport) {
|
|
|
4894
5194
|
selectors[check.selector] = selectors[check.selector] || await selectorStats(check.selector);
|
|
4895
5195
|
frames[check.selector] = frames[check.selector] || await frameEvidence(check.selector);
|
|
4896
5196
|
}
|
|
5197
|
+
if (check.type === "http_status") {
|
|
5198
|
+
const key = httpStatusKey(check, page.url() || targetUrl);
|
|
5199
|
+
http_statuses[key] = http_statuses[key] || await collectHttpStatus(check);
|
|
5200
|
+
}
|
|
4897
5201
|
if (check.type === "link_status" || check.type === "artifact_link_status") {
|
|
4898
5202
|
const selector = linkStatusSelector(check);
|
|
4899
5203
|
link_statuses[selector] = link_statuses[selector] || await collectLinkStatus(check);
|
|
@@ -4961,6 +5265,7 @@ async function captureViewport(viewport) {
|
|
|
4961
5265
|
frames,
|
|
4962
5266
|
text_sequences,
|
|
4963
5267
|
text_matches,
|
|
5268
|
+
http_statuses,
|
|
4964
5269
|
link_statuses,
|
|
4965
5270
|
route_inventory: routeInventory,
|
|
4966
5271
|
setup_action_results: setupActionResults,
|
|
@@ -5010,6 +5315,19 @@ function buildProfileEvidence(currentViewports) {
|
|
|
5010
5315
|
),
|
|
5011
5316
|
})),
|
|
5012
5317
|
})),
|
|
5318
|
+
http_status: currentViewports
|
|
5319
|
+
.filter((viewport) => viewport.http_statuses && Object.keys(viewport.http_statuses).length)
|
|
5320
|
+
.map((viewport) => ({
|
|
5321
|
+
viewport: viewport.name,
|
|
5322
|
+
requests: Object.entries(viewport.http_statuses || {}).map(([key, statusSet]) => ({
|
|
5323
|
+
key,
|
|
5324
|
+
url: statusSet && statusSet.url,
|
|
5325
|
+
method: statusSet && statusSet.method,
|
|
5326
|
+
status: statusSet && statusSet.status,
|
|
5327
|
+
ok: statusSet && statusSet.ok === true,
|
|
5328
|
+
error: statusSet && statusSet.error,
|
|
5329
|
+
})),
|
|
5330
|
+
})),
|
|
5013
5331
|
link_status: currentViewports
|
|
5014
5332
|
.filter((viewport) => viewport.link_statuses && Object.keys(viewport.link_statuses).length)
|
|
5015
5333
|
.map((viewport) => ({
|