@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.
@@ -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 expectedStatus = isLinkStatusCheck ? normalizeHttpStatus(input.expected_status ?? input.expectedStatus ?? input.status, `checks[${index}] expected_status`) : void 0;
783
- const allowedStatuses = isLinkStatusCheck ? normalizeHttpStatuses(
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 = isLinkStatusCheck ? normalizePositiveInteger(input.min_bytes ?? input.minBytes ?? input.min_response_bytes ?? input.minResponseBytes, `checks[${index}] min_bytes`) : void 0;
789
- const expectedContentType = isLinkStatusCheck ? stringValue(input.expected_content_type) || stringValue(input.expectedContentType) || stringValue(input.content_type) || stringValue(input.contentType) : void 0;
790
- const allowedContentTypes = isLinkStatusCheck ? normalizeStringList(
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: isLinkStatusCheck ? input.require_nonzero_bytes === true || input.requireNonzeroBytes === true : void 0,
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 linkStatusAllowedStatuses(check) {
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 linkStatusIsAllowed(status, check) {
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 = linkStatusAllowedStatuses(check);
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 (!linkStatusIsAllowed(status, check)) return false;
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 linkStatusAllowedStatuses(check) {
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 linkStatusIsAllowed(status, check) {
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 = linkStatusAllowedStatuses(check);
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 (!linkStatusIsAllowed(result.status, check)) return false;
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) => ({