@riddledc/riddle-proof 0.7.126 → 0.7.128

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/index.cjs CHANGED
@@ -8872,6 +8872,9 @@ function valueFromOwn(input, ...keys) {
8872
8872
  function numberValue3(value) {
8873
8873
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
8874
8874
  }
8875
+ function booleanValue(value) {
8876
+ return typeof value === "boolean" ? value : void 0;
8877
+ }
8875
8878
  function horizontalBoundsOverflowPx2(value) {
8876
8879
  if (!isRecord2(value)) return 0;
8877
8880
  let max = maxPositiveNumber2(
@@ -8966,6 +8969,187 @@ function toJsonValue(value) {
8966
8969
  if (isRecord2(value)) return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, toJsonValue(child)]));
8967
8970
  return String(value);
8968
8971
  }
8972
+ function jsonValueType(value) {
8973
+ if (value === null) return "null";
8974
+ if (Array.isArray(value)) return "array";
8975
+ if (typeof value === "boolean") return "boolean";
8976
+ if (typeof value === "number") return "number";
8977
+ if (typeof value === "string") return "string";
8978
+ return "object";
8979
+ }
8980
+ function compactJsonAssertionSample(value, depth = 0) {
8981
+ if (typeof value === "string") return value.length > 240 ? `${value.slice(0, 237)}...` : value;
8982
+ if (value === null || typeof value === "boolean" || typeof value === "number") return toJsonValue(value);
8983
+ if (Array.isArray(value)) {
8984
+ if (depth >= 2) return `[array:${value.length}]`;
8985
+ return value.slice(0, 3).map((item) => compactJsonAssertionSample(item, depth + 1));
8986
+ }
8987
+ if (isRecord2(value)) {
8988
+ const entries = Object.entries(value).slice(0, 8);
8989
+ if (depth >= 2) return `[object:${Object.keys(value).length} keys]`;
8990
+ return Object.fromEntries(entries.map(([key, child]) => [key, compactJsonAssertionSample(child, depth + 1)]));
8991
+ }
8992
+ return String(value);
8993
+ }
8994
+ function attachJsonAssertionObservedValue(result, value) {
8995
+ const type = jsonValueType(value);
8996
+ if (type === "array" && Array.isArray(value)) {
8997
+ result.observed_length = value.length;
8998
+ result.observed_omitted_count = Math.max(0, value.length - 3);
8999
+ result.observed_sample = compactJsonAssertionSample(value);
9000
+ return;
9001
+ }
9002
+ if (type === "object" && isRecord2(value)) {
9003
+ const keyCount = Object.keys(value).length;
9004
+ result.observed_key_count = keyCount;
9005
+ result.observed_omitted_count = Math.max(0, keyCount - 8);
9006
+ result.observed_sample = compactJsonAssertionSample(value);
9007
+ return;
9008
+ }
9009
+ result.observed = toJsonValue(value);
9010
+ }
9011
+ function deepJsonEqual(left, right) {
9012
+ if (left === right) return true;
9013
+ if (typeof left !== typeof right) return false;
9014
+ if (left === null || right === null) return left === right;
9015
+ if (typeof left !== "object" || typeof right !== "object") return false;
9016
+ if (Array.isArray(left) || Array.isArray(right)) {
9017
+ if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) return false;
9018
+ return left.every((item, index) => deepJsonEqual(item, right[index]));
9019
+ }
9020
+ if (!isRecord2(left) || !isRecord2(right)) return false;
9021
+ const leftKeys = Object.keys(left).sort();
9022
+ const rightKeys = Object.keys(right).sort();
9023
+ if (!deepJsonEqual(leftKeys, rightKeys)) return false;
9024
+ return leftKeys.every((key) => deepJsonEqual(left[key], right[key]));
9025
+ }
9026
+ function jsonContains(observed, expected) {
9027
+ if (typeof observed === "string" && typeof expected === "string") {
9028
+ return observed.includes(expected);
9029
+ }
9030
+ if (Array.isArray(observed)) {
9031
+ return observed.some((item) => deepJsonEqual(item, expected));
9032
+ }
9033
+ if (isRecord2(observed) && isRecord2(expected)) {
9034
+ return Object.entries(expected).every(([key, value]) => hasOwn(observed, key) && deepJsonEqual(observed[key], value));
9035
+ }
9036
+ return false;
9037
+ }
9038
+ function parseJsonPathSegments(path6) {
9039
+ let input = path6.trim();
9040
+ if (!input) throw new Error("path is empty");
9041
+ if (input === "$") return [];
9042
+ if (input.startsWith("$.")) input = input.slice(2);
9043
+ else if (input.startsWith("$[")) input = input.slice(1);
9044
+ const segments = [];
9045
+ let token = "";
9046
+ const pushToken = () => {
9047
+ if (!token) return;
9048
+ segments.push(token);
9049
+ token = "";
9050
+ };
9051
+ for (let index = 0; index < input.length; index += 1) {
9052
+ const char = input[index];
9053
+ if (char === ".") {
9054
+ pushToken();
9055
+ continue;
9056
+ }
9057
+ if (char !== "[") {
9058
+ token += char;
9059
+ continue;
9060
+ }
9061
+ pushToken();
9062
+ const closeIndex = input.indexOf("]", index + 1);
9063
+ if (closeIndex === -1) throw new Error(`unterminated bracket at ${index}`);
9064
+ const bracket = input.slice(index + 1, closeIndex).trim();
9065
+ if (!bracket) throw new Error(`empty bracket at ${index}`);
9066
+ if (/^\d+$/.test(bracket)) {
9067
+ segments.push(Number(bracket));
9068
+ } else if (bracket.startsWith('"') && bracket.endsWith('"') || bracket.startsWith("'") && bracket.endsWith("'")) {
9069
+ const quoted = bracket.startsWith("'") ? `"${bracket.slice(1, -1).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"` : bracket;
9070
+ segments.push(String(JSON.parse(quoted)));
9071
+ } else {
9072
+ segments.push(bracket);
9073
+ }
9074
+ index = closeIndex;
9075
+ }
9076
+ pushToken();
9077
+ return segments;
9078
+ }
9079
+ function resolveJsonPath(root, path6) {
9080
+ let segments;
9081
+ try {
9082
+ segments = parseJsonPathSegments(path6);
9083
+ } catch (error) {
9084
+ return { exists: false, error: String(error instanceof Error ? error.message : error) };
9085
+ }
9086
+ let current = root;
9087
+ for (const segment of segments) {
9088
+ if (typeof segment === "number") {
9089
+ if (!Array.isArray(current) || segment < 0 || segment >= current.length) return { exists: false };
9090
+ current = current[segment];
9091
+ continue;
9092
+ }
9093
+ if (!isRecord2(current) || !hasOwn(current, segment)) return { exists: false };
9094
+ current = current[segment];
9095
+ }
9096
+ return { exists: true, value: current };
9097
+ }
9098
+ function evaluateHttpStatusBodyJsonAssertion(root, assertion) {
9099
+ const resolved = resolveJsonPath(root, assertion.path);
9100
+ const errors = [];
9101
+ const result = {
9102
+ label: assertion.label || assertion.path,
9103
+ path: assertion.path,
9104
+ ok: true,
9105
+ exists: resolved.exists,
9106
+ observed_type: resolved.exists ? jsonValueType(resolved.value) : "missing"
9107
+ };
9108
+ if (resolved.exists) attachJsonAssertionObservedValue(result, resolved.value);
9109
+ if (resolved.error) errors.push(resolved.error);
9110
+ if (hasOwn(assertion, "exists")) {
9111
+ result.expected_exists = assertion.exists;
9112
+ if (resolved.exists !== assertion.exists) errors.push(`expected exists=${assertion.exists}`);
9113
+ }
9114
+ if (hasOwn(assertion, "type")) {
9115
+ result.type = assertion.type;
9116
+ if (!resolved.exists || jsonValueType(resolved.value) !== assertion.type) errors.push(`expected type ${assertion.type}`);
9117
+ }
9118
+ if (hasOwn(assertion, "equals")) {
9119
+ result.equals = assertion.equals;
9120
+ if (!resolved.exists || !deepJsonEqual(resolved.value, assertion.equals)) errors.push("expected JSON value equality");
9121
+ }
9122
+ if (hasOwn(assertion, "not_equals")) {
9123
+ result.not_equals = assertion.not_equals;
9124
+ if (resolved.exists && deepJsonEqual(resolved.value, assertion.not_equals)) errors.push("expected JSON value inequality");
9125
+ }
9126
+ if (hasOwn(assertion, "contains")) {
9127
+ result.contains = assertion.contains;
9128
+ if (!resolved.exists || !jsonContains(resolved.value, assertion.contains)) errors.push("expected JSON value containment");
9129
+ }
9130
+ result.ok = errors.length === 0;
9131
+ if (errors.length) result.errors = errors;
9132
+ return result;
9133
+ }
9134
+ function evaluateHttpStatusBodyJsonAssertions(bodyText, assertions) {
9135
+ const expected = assertions?.filter((assertion) => assertion.path) ?? [];
9136
+ if (!expected.length) return [];
9137
+ let parsed;
9138
+ try {
9139
+ parsed = JSON.parse(bodyText);
9140
+ } catch (error) {
9141
+ const message = `response body is not valid JSON: ${String(error instanceof Error ? error.message : error).slice(0, 200)}`;
9142
+ return expected.map((assertion) => ({
9143
+ label: assertion.label || assertion.path,
9144
+ path: assertion.path,
9145
+ ok: false,
9146
+ exists: false,
9147
+ observed_type: "missing",
9148
+ errors: [message]
9149
+ }));
9150
+ }
9151
+ return expected.map((assertion) => evaluateHttpStatusBodyJsonAssertion(parsed, assertion));
9152
+ }
8969
9153
  function compactProfileSetupSummaryText(value, limit = 160) {
8970
9154
  const text = typeof value === "string" ? value.replace(/\s+/g, " ").trim() : "";
8971
9155
  if (!text) return void 0;
@@ -9620,6 +9804,46 @@ function validateRegexPatterns(patterns, label) {
9620
9804
  }
9621
9805
  }
9622
9806
  }
9807
+ function normalizeHttpStatusBodyJsonAssertions(value, label) {
9808
+ if (value === void 0) return void 0;
9809
+ if (!Array.isArray(value)) throw new Error(`${label} must be an array.`);
9810
+ if (!value.length) throw new Error(`${label} must not be empty.`);
9811
+ return value.map((item, index) => {
9812
+ const itemLabel = `${label}[${index}]`;
9813
+ if (typeof item === "string") {
9814
+ const path7 = stringValue5(item);
9815
+ if (!path7) throw new Error(`${itemLabel} path must not be empty.`);
9816
+ return { path: path7, exists: true };
9817
+ }
9818
+ if (!isRecord2(item)) throw new Error(`${itemLabel} must be an object or JSON path string.`);
9819
+ const path6 = stringFromOwn(item, "path", "json_path", "jsonPath", "key");
9820
+ if (!path6) throw new Error(`${itemLabel}.path is required.`);
9821
+ const assertion = {
9822
+ label: stringValue5(item.label),
9823
+ path: path6
9824
+ };
9825
+ const exists = booleanValue(valueFromOwn(item, "exists", "present"));
9826
+ if (exists !== void 0) assertion.exists = exists;
9827
+ const type = stringValue5(valueFromOwn(item, "type", "value_type", "valueType"));
9828
+ if (type !== void 0) {
9829
+ const allowedTypes = ["array", "boolean", "null", "number", "object", "string"];
9830
+ if (!allowedTypes.includes(type)) {
9831
+ throw new Error(`${itemLabel}.type must be one of ${allowedTypes.join(", ")}.`);
9832
+ }
9833
+ assertion.type = type;
9834
+ }
9835
+ const equalsValue = valueFromOwn(item, "equals", "expected", "expected_value", "expectedValue", "value");
9836
+ if (equalsValue !== void 0) assertion.equals = toJsonValue(equalsValue);
9837
+ const notEqualsValue = valueFromOwn(item, "not_equals", "notEquals", "forbidden", "forbidden_value", "forbiddenValue");
9838
+ if (notEqualsValue !== void 0) assertion.not_equals = toJsonValue(notEqualsValue);
9839
+ const containsValue = valueFromOwn(item, "contains", "includes", "contains_value", "containsValue", "include");
9840
+ if (containsValue !== void 0) assertion.contains = toJsonValue(containsValue);
9841
+ if (assertion.exists === void 0 && assertion.type === void 0 && !hasOwn(assertion, "equals") && !hasOwn(assertion, "not_equals") && !hasOwn(assertion, "contains")) {
9842
+ assertion.exists = true;
9843
+ }
9844
+ return assertion;
9845
+ });
9846
+ }
9623
9847
  function isDialogCountCheckType(type) {
9624
9848
  return type === "dialog_count_equals" || type === "dialog_accept_count_equals" || type === "dialog_dismiss_count_equals";
9625
9849
  }
@@ -9719,6 +9943,10 @@ function normalizeCheck(input, index) {
9719
9943
  `checks[${index}] body_not_patterns`
9720
9944
  ) : void 0;
9721
9945
  if (bodyNotPatterns?.length) validateRegexPatterns(bodyNotPatterns, `checks[${index}] body_not_patterns`);
9946
+ const bodyJsonAssertions = isHttpStatusCheck ? normalizeHttpStatusBodyJsonAssertions(
9947
+ input.body_json_assertions ?? input.bodyJsonAssertions ?? input.json_body_assertions ?? input.jsonBodyAssertions ?? input.json_assertions ?? input.jsonAssertions ?? input.response_json_assertions ?? input.responseJsonAssertions,
9948
+ `checks[${index}] body_json_assertions`
9949
+ ) : void 0;
9722
9950
  if (isLinkStatusCheck) {
9723
9951
  if (minCount !== void 0 && (!Number.isInteger(minCount) || minCount < 0)) {
9724
9952
  throw new Error(`checks[${index}] ${type} min_count must be a non-negative integer.`);
@@ -9744,6 +9972,7 @@ function normalizeCheck(input, index) {
9744
9972
  body_contains: bodyContains,
9745
9973
  body_not_contains: bodyNotContains,
9746
9974
  body_not_patterns: bodyNotPatterns,
9975
+ body_json_assertions: bodyJsonAssertions,
9747
9976
  expected_texts: expectedTexts,
9748
9977
  link_selector: stringValue5(input.link_selector) || stringValue5(input.linkSelector),
9749
9978
  source_selector: stringValue5(input.source_selector) || stringValue5(input.sourceSelector),
@@ -9950,6 +10179,38 @@ function httpStatusBodyNotPatternFailures(result, check) {
9950
10179
  const observed = isRecord2(result.body_not_patterns) ? result.body_not_patterns : {};
9951
10180
  return forbidden.filter((pattern) => observed[pattern] !== false);
9952
10181
  }
10182
+ function httpStatusBodyJsonAssertionFailures(result, check) {
10183
+ const expected = check.body_json_assertions?.filter((assertion) => assertion.path) ?? [];
10184
+ if (!expected.length) return [];
10185
+ if (!Array.isArray(result.body_json_assertions)) {
10186
+ return expected.map((assertion) => ({
10187
+ label: assertion.label || assertion.path,
10188
+ path: assertion.path,
10189
+ ok: false,
10190
+ exists: false,
10191
+ observed_type: "missing",
10192
+ errors: ["body_json_assertions evidence missing"]
10193
+ }));
10194
+ }
10195
+ return result.body_json_assertions.filter((assertion) => isRecord2(assertion) && assertion.ok !== true).map((assertion) => ({
10196
+ label: stringValue5(assertion.label) || stringValue5(assertion.path) || "json assertion",
10197
+ path: stringValue5(assertion.path) || "",
10198
+ ok: false,
10199
+ exists: assertion.exists === true,
10200
+ observed: hasOwn(assertion, "observed") ? toJsonValue(assertion.observed) : void 0,
10201
+ observed_sample: hasOwn(assertion, "observed_sample") ? toJsonValue(assertion.observed_sample) : void 0,
10202
+ observed_length: numberValue3(assertion.observed_length),
10203
+ observed_key_count: numberValue3(assertion.observed_key_count),
10204
+ observed_omitted_count: numberValue3(assertion.observed_omitted_count),
10205
+ observed_type: stringValue5(assertion.observed_type) || "missing",
10206
+ expected_exists: booleanValue(assertion.expected_exists),
10207
+ equals: hasOwn(assertion, "equals") ? toJsonValue(assertion.equals) : void 0,
10208
+ not_equals: hasOwn(assertion, "not_equals") ? toJsonValue(assertion.not_equals) : void 0,
10209
+ contains: hasOwn(assertion, "contains") ? toJsonValue(assertion.contains) : void 0,
10210
+ type: stringValue5(assertion.type),
10211
+ errors: Array.isArray(assertion.errors) ? assertion.errors.map(String) : void 0
10212
+ }));
10213
+ }
9953
10214
  function linkStatusResultOk(result, check) {
9954
10215
  const status = numberValue3(result.status);
9955
10216
  if (!httpStatusIsAllowed(status, check)) return false;
@@ -9967,6 +10228,7 @@ function linkStatusResultOk(result, check) {
9967
10228
  if (httpStatusBodyContainsFailures(result, check).length) return false;
9968
10229
  if (httpStatusBodyNotContainsFailures(result, check).length) return false;
9969
10230
  if (httpStatusBodyNotPatternFailures(result, check).length) return false;
10231
+ if (httpStatusBodyJsonAssertionFailures(result, check).length) return false;
9970
10232
  return true;
9971
10233
  }
9972
10234
  function responseHeader(response, name) {
@@ -10035,7 +10297,7 @@ async function preflightHttpStatusCheck(check, index, targetUrl, fetchImpl) {
10035
10297
  statusText = typeof response.statusText === "string" ? response.statusText : "";
10036
10298
  result.content_type = responseHeader(response, "content-type");
10037
10299
  result.content_length = responseContentLength(response);
10038
- const shouldReadBody = check.require_nonzero_bytes === true || typeof check.min_bytes === "number" || Boolean(check.body_contains?.length) || Boolean(check.body_not_contains?.length) || Boolean(check.body_not_patterns?.length);
10300
+ const shouldReadBody = check.require_nonzero_bytes === true || typeof check.min_bytes === "number" || Boolean(check.body_contains?.length) || Boolean(check.body_not_contains?.length) || Boolean(check.body_not_patterns?.length) || Boolean(check.body_json_assertions?.length);
10039
10301
  if (shouldReadBody && method !== "HEAD") {
10040
10302
  const body = await responseBodyText(response);
10041
10303
  result.bytes = body.bytes;
@@ -10048,6 +10310,9 @@ async function preflightHttpStatusCheck(check, index, targetUrl, fetchImpl) {
10048
10310
  if (check.body_not_patterns?.length) {
10049
10311
  result.body_not_patterns = Object.fromEntries(check.body_not_patterns.filter(Boolean).map((pattern) => [pattern, new RegExp(pattern).test(body.text)]));
10050
10312
  }
10313
+ if (check.body_json_assertions?.length) {
10314
+ result.body_json_assertions = evaluateHttpStatusBodyJsonAssertions(body.text, check.body_json_assertions);
10315
+ }
10051
10316
  }
10052
10317
  } catch (caught) {
10053
10318
  error = String(caught instanceof Error ? caught.message : caught).slice(0, 500);
@@ -10056,6 +10321,7 @@ async function preflightHttpStatusCheck(check, index, targetUrl, fetchImpl) {
10056
10321
  const bodyContainsMissing = httpStatusBodyContainsFailures(result, check);
10057
10322
  const bodyNotContainsFound = httpStatusBodyNotContainsFailures(result, check);
10058
10323
  const bodyNotPatternsFound = httpStatusBodyNotPatternFailures(result, check);
10324
+ const bodyJsonAssertionsFailed = httpStatusBodyJsonAssertionFailures(result, check);
10059
10325
  const ok = !error && linkStatusResultOk(result, check);
10060
10326
  return {
10061
10327
  index,
@@ -10074,7 +10340,9 @@ async function preflightHttpStatusCheck(check, index, targetUrl, fetchImpl) {
10074
10340
  body_not_contains: isRecord2(result.body_not_contains) ? Object.fromEntries(Object.entries(result.body_not_contains).map(([key, value]) => [key, value === true])) : null,
10075
10341
  body_not_contains_found: bodyNotContainsFound,
10076
10342
  body_not_patterns: isRecord2(result.body_not_patterns) ? Object.fromEntries(Object.entries(result.body_not_patterns).map(([key, value]) => [key, value === true])) : null,
10077
- body_not_patterns_found: bodyNotPatternsFound
10343
+ body_not_patterns_found: bodyNotPatternsFound,
10344
+ body_json_assertions: Array.isArray(result.body_json_assertions) ? result.body_json_assertions : null,
10345
+ body_json_assertions_failed: bodyJsonAssertionsFailed
10078
10346
  };
10079
10347
  }
10080
10348
  async function preflightRiddleProofProfileHttpStatusChecks(profile, options = {}) {
@@ -10119,6 +10387,7 @@ function summarizeHttpStatusEvidence(viewport, check) {
10119
10387
  const bodyContainsMissing = httpStatusBodyContainsFailures(statusEvidence, check);
10120
10388
  const bodyNotContainsFound = httpStatusBodyNotContainsFailures(statusEvidence, check);
10121
10389
  const bodyNotPatternsFound = httpStatusBodyNotPatternFailures(statusEvidence, check);
10390
+ const bodyJsonAssertionsFailed = httpStatusBodyJsonAssertionFailures(statusEvidence, check);
10122
10391
  if (!linkStatusResultOk(statusEvidence, check)) {
10123
10392
  failures.push({
10124
10393
  code: "http_status_failed",
@@ -10137,6 +10406,8 @@ function summarizeHttpStatusEvidence(viewport, check) {
10137
10406
  body_not_contains_found: bodyNotContainsFound,
10138
10407
  body_not_patterns: check.body_not_patterns ?? null,
10139
10408
  body_not_patterns_found: bodyNotPatternsFound,
10409
+ body_json_assertions: check.body_json_assertions ?? null,
10410
+ body_json_assertions_failed: bodyJsonAssertionsFailed.map((assertion) => toJsonValue(assertion)),
10140
10411
  body_sample: stringValue5(statusEvidence.body_sample) ?? null
10141
10412
  });
10142
10413
  }
@@ -10158,6 +10429,8 @@ function summarizeHttpStatusEvidence(viewport, check) {
10158
10429
  body_not_contains_found: bodyNotContainsFound,
10159
10430
  body_not_patterns: isRecord2(statusEvidence.body_not_patterns) ? toJsonValue(statusEvidence.body_not_patterns) : null,
10160
10431
  body_not_patterns_found: bodyNotPatternsFound,
10432
+ body_json_assertions: Array.isArray(statusEvidence.body_json_assertions) ? toJsonValue(statusEvidence.body_json_assertions) : null,
10433
+ body_json_assertions_failed: bodyJsonAssertionsFailed.map((assertion) => toJsonValue(assertion)),
10161
10434
  body_sample: stringValue5(statusEvidence.body_sample) ?? null,
10162
10435
  failures
10163
10436
  };
@@ -10794,6 +11067,7 @@ function assessCheckFromEvidence(check, evidence) {
10794
11067
  body_contains: check.body_contains ?? [],
10795
11068
  body_not_contains: check.body_not_contains ?? [],
10796
11069
  body_not_patterns: check.body_not_patterns ?? [],
11070
+ body_json_assertions: toJsonValue(check.body_json_assertions ?? []),
10797
11071
  viewports: summaries.map((summary) => toJsonValue(summary)),
10798
11072
  failures: failed.flatMap((summary) => Array.isArray(summary.failures) ? summary.failures.map((failure) => toJsonValue({ viewport: stringValue5(summary.viewport) ?? null, failure })) : [])
10799
11073
  },
@@ -11537,6 +11811,40 @@ function httpStatusBodyNotPatternFailures(result, check) {
11537
11811
  : {};
11538
11812
  return forbidden.filter((pattern) => observed[pattern] !== false);
11539
11813
  }
11814
+ function httpStatusBodyJsonAssertionFailures(result, check) {
11815
+ const expected = Array.isArray(check.body_json_assertions) ? check.body_json_assertions.filter((assertion) => assertion && assertion.path) : [];
11816
+ if (!expected.length) return [];
11817
+ if (!Array.isArray(result.body_json_assertions)) {
11818
+ return expected.map((assertion) => ({
11819
+ label: assertion.label || assertion.path,
11820
+ path: assertion.path,
11821
+ ok: false,
11822
+ exists: false,
11823
+ observed_type: "missing",
11824
+ errors: ["body_json_assertions evidence missing"],
11825
+ }));
11826
+ }
11827
+ return result.body_json_assertions
11828
+ .filter((assertion) => assertion && typeof assertion === "object" && assertion.ok !== true)
11829
+ .map((assertion) => ({
11830
+ label: typeof assertion.label === "string" && assertion.label ? assertion.label : typeof assertion.path === "string" && assertion.path ? assertion.path : "json assertion",
11831
+ path: typeof assertion.path === "string" ? assertion.path : "",
11832
+ ok: false,
11833
+ exists: assertion.exists === true,
11834
+ observed: Object.hasOwn(assertion, "observed") ? assertion.observed : undefined,
11835
+ observed_sample: Object.hasOwn(assertion, "observed_sample") ? assertion.observed_sample : undefined,
11836
+ observed_length: typeof assertion.observed_length === "number" && Number.isFinite(assertion.observed_length) ? assertion.observed_length : undefined,
11837
+ observed_key_count: typeof assertion.observed_key_count === "number" && Number.isFinite(assertion.observed_key_count) ? assertion.observed_key_count : undefined,
11838
+ observed_omitted_count: typeof assertion.observed_omitted_count === "number" && Number.isFinite(assertion.observed_omitted_count) ? assertion.observed_omitted_count : undefined,
11839
+ observed_type: typeof assertion.observed_type === "string" && assertion.observed_type ? assertion.observed_type : "missing",
11840
+ expected_exists: typeof assertion.expected_exists === "boolean" ? assertion.expected_exists : undefined,
11841
+ equals: Object.hasOwn(assertion, "equals") ? assertion.equals : undefined,
11842
+ not_equals: Object.hasOwn(assertion, "not_equals") ? assertion.not_equals : undefined,
11843
+ contains: Object.hasOwn(assertion, "contains") ? assertion.contains : undefined,
11844
+ type: typeof assertion.type === "string" ? assertion.type : undefined,
11845
+ errors: Array.isArray(assertion.errors) ? assertion.errors.map(String) : undefined,
11846
+ }));
11847
+ }
11540
11848
  function linkStatusResultOk(result, check) {
11541
11849
  if (!result || typeof result !== "object" || Array.isArray(result)) return false;
11542
11850
  if (!httpStatusIsAllowed(result.status, check)) return false;
@@ -11554,6 +11862,7 @@ function linkStatusResultOk(result, check) {
11554
11862
  if (httpStatusBodyContainsFailures(result, check).length) return false;
11555
11863
  if (httpStatusBodyNotContainsFailures(result, check).length) return false;
11556
11864
  if (httpStatusBodyNotPatternFailures(result, check).length) return false;
11865
+ if (httpStatusBodyJsonAssertionFailures(result, check).length) return false;
11557
11866
  return true;
11558
11867
  }
11559
11868
  function summarizeHttpStatusEvidence(viewport, check) {
@@ -11574,6 +11883,7 @@ function summarizeHttpStatusEvidence(viewport, check) {
11574
11883
  const bodyContainsMissing = httpStatusBodyContainsFailures(statusEvidence, check);
11575
11884
  const bodyNotContainsFound = httpStatusBodyNotContainsFailures(statusEvidence, check);
11576
11885
  const bodyNotPatternsFound = httpStatusBodyNotPatternFailures(statusEvidence, check);
11886
+ const bodyJsonAssertionsFailed = httpStatusBodyJsonAssertionFailures(statusEvidence, check);
11577
11887
  if (!linkStatusResultOk(statusEvidence, check)) {
11578
11888
  failures.push({
11579
11889
  code: "http_status_failed",
@@ -11592,6 +11902,8 @@ function summarizeHttpStatusEvidence(viewport, check) {
11592
11902
  body_not_contains_found: bodyNotContainsFound,
11593
11903
  body_not_patterns: Array.isArray(check.body_not_patterns) ? check.body_not_patterns : null,
11594
11904
  body_not_patterns_found: bodyNotPatternsFound,
11905
+ body_json_assertions: Array.isArray(check.body_json_assertions) ? check.body_json_assertions : null,
11906
+ body_json_assertions_failed: bodyJsonAssertionsFailed,
11595
11907
  body_sample: typeof statusEvidence.body_sample === "string" ? statusEvidence.body_sample : null,
11596
11908
  });
11597
11909
  }
@@ -11619,6 +11931,8 @@ function summarizeHttpStatusEvidence(viewport, check) {
11619
11931
  ? statusEvidence.body_not_patterns
11620
11932
  : null,
11621
11933
  body_not_patterns_found: bodyNotPatternsFound,
11934
+ body_json_assertions: Array.isArray(statusEvidence.body_json_assertions) ? statusEvidence.body_json_assertions : null,
11935
+ body_json_assertions_failed: bodyJsonAssertionsFailed,
11622
11936
  body_sample: typeof statusEvidence.body_sample === "string" ? statusEvidence.body_sample : null,
11623
11937
  failures,
11624
11938
  };
@@ -13721,6 +14035,187 @@ function linkProbeResponseFields(response, method) {
13721
14035
  content_length: contentLength,
13722
14036
  };
13723
14037
  }
14038
+ function jsonProbeValueType(value) {
14039
+ if (value === null) return "null";
14040
+ if (Array.isArray(value)) return "array";
14041
+ if (typeof value === "boolean") return "boolean";
14042
+ if (typeof value === "number") return "number";
14043
+ if (typeof value === "string") return "string";
14044
+ return "object";
14045
+ }
14046
+ function compactJsonProbeSample(value, depth) {
14047
+ const level = typeof depth === "number" ? depth : 0;
14048
+ if (typeof value === "string") return value.length > 240 ? value.slice(0, 237) + "..." : value;
14049
+ if (value === null || typeof value === "boolean" || typeof value === "number") return value;
14050
+ if (Array.isArray(value)) {
14051
+ if (level >= 2) return "[array:" + value.length + "]";
14052
+ return value.slice(0, 3).map((item) => compactJsonProbeSample(item, level + 1));
14053
+ }
14054
+ if (value && typeof value === "object") {
14055
+ const entries = Object.entries(value);
14056
+ if (level >= 2) return "[object:" + entries.length + " keys]";
14057
+ return Object.fromEntries(entries.slice(0, 8).map(([key, child]) => [key, compactJsonProbeSample(child, level + 1)]));
14058
+ }
14059
+ return String(value);
14060
+ }
14061
+ function attachJsonProbeObservedValue(result, value) {
14062
+ const type = jsonProbeValueType(value);
14063
+ if (type === "array" && Array.isArray(value)) {
14064
+ result.observed_length = value.length;
14065
+ result.observed_omitted_count = Math.max(0, value.length - 3);
14066
+ result.observed_sample = compactJsonProbeSample(value, 0);
14067
+ return;
14068
+ }
14069
+ if (type === "object" && value && typeof value === "object" && !Array.isArray(value)) {
14070
+ const keyCount = Object.keys(value).length;
14071
+ result.observed_key_count = keyCount;
14072
+ result.observed_omitted_count = Math.max(0, keyCount - 8);
14073
+ result.observed_sample = compactJsonProbeSample(value, 0);
14074
+ return;
14075
+ }
14076
+ result.observed = value;
14077
+ }
14078
+ function jsonProbeDeepEqual(left, right) {
14079
+ if (left === right) return true;
14080
+ if (typeof left !== typeof right) return false;
14081
+ if (left === null || right === null) return left === right;
14082
+ if (typeof left !== "object" || typeof right !== "object") return false;
14083
+ if (Array.isArray(left) || Array.isArray(right)) {
14084
+ if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) return false;
14085
+ return left.every((item, index) => jsonProbeDeepEqual(item, right[index]));
14086
+ }
14087
+ const leftKeys = Object.keys(left).sort();
14088
+ const rightKeys = Object.keys(right).sort();
14089
+ if (!jsonProbeDeepEqual(leftKeys, rightKeys)) return false;
14090
+ return leftKeys.every((key) => jsonProbeDeepEqual(left[key], right[key]));
14091
+ }
14092
+ function jsonProbeContains(observed, expected) {
14093
+ if (typeof observed === "string" && typeof expected === "string") return observed.includes(expected);
14094
+ if (Array.isArray(observed)) return observed.some((item) => jsonProbeDeepEqual(item, expected));
14095
+ if (observed && expected && typeof observed === "object" && typeof expected === "object" && !Array.isArray(observed) && !Array.isArray(expected)) {
14096
+ return Object.entries(expected).every(([key, value]) => Object.hasOwn(observed, key) && jsonProbeDeepEqual(observed[key], value));
14097
+ }
14098
+ return false;
14099
+ }
14100
+ function parseJsonProbePathSegments(path) {
14101
+ let input = String(path || "").trim();
14102
+ if (!input) throw new Error("path is empty");
14103
+ if (input === "$") return [];
14104
+ if (input.startsWith("$.")) input = input.slice(2);
14105
+ else if (input.startsWith("$[")) input = input.slice(1);
14106
+ const segments = [];
14107
+ let token = "";
14108
+ const pushToken = () => {
14109
+ if (!token) return;
14110
+ segments.push(token);
14111
+ token = "";
14112
+ };
14113
+ for (let index = 0; index < input.length; index += 1) {
14114
+ const char = input[index];
14115
+ if (char === ".") {
14116
+ pushToken();
14117
+ continue;
14118
+ }
14119
+ if (char !== "[") {
14120
+ token += char;
14121
+ continue;
14122
+ }
14123
+ pushToken();
14124
+ const closeIndex = input.indexOf("]", index + 1);
14125
+ if (closeIndex === -1) throw new Error("unterminated bracket at " + index);
14126
+ const bracket = input.slice(index + 1, closeIndex).trim();
14127
+ if (!bracket) throw new Error("empty bracket at " + index);
14128
+ if (/^\d+$/.test(bracket)) {
14129
+ segments.push(Number(bracket));
14130
+ } else if ((bracket.startsWith('"') && bracket.endsWith('"')) || (bracket.startsWith("'") && bracket.endsWith("'"))) {
14131
+ const quoted = bracket.startsWith("'")
14132
+ ? '"' + bracket.slice(1, -1).replace(/\\/g, "\\\\").replace(/"/g, "\\\"") + '"'
14133
+ : bracket;
14134
+ segments.push(String(JSON.parse(quoted)));
14135
+ } else {
14136
+ segments.push(bracket);
14137
+ }
14138
+ index = closeIndex;
14139
+ }
14140
+ pushToken();
14141
+ return segments;
14142
+ }
14143
+ function resolveJsonProbePath(root, path) {
14144
+ let segments;
14145
+ try {
14146
+ segments = parseJsonProbePathSegments(path);
14147
+ } catch (error) {
14148
+ return { exists: false, error: String(error && error.message ? error.message : error) };
14149
+ }
14150
+ let current = root;
14151
+ for (const segment of segments) {
14152
+ if (typeof segment === "number") {
14153
+ if (!Array.isArray(current) || segment < 0 || segment >= current.length) return { exists: false };
14154
+ current = current[segment];
14155
+ continue;
14156
+ }
14157
+ if (!current || typeof current !== "object" || Array.isArray(current) || !Object.hasOwn(current, segment)) {
14158
+ return { exists: false };
14159
+ }
14160
+ current = current[segment];
14161
+ }
14162
+ return { exists: true, value: current };
14163
+ }
14164
+ function evaluateJsonProbeAssertion(root, assertion) {
14165
+ const resolved = resolveJsonProbePath(root, assertion.path);
14166
+ const errors = [];
14167
+ const result = {
14168
+ label: assertion.label || assertion.path,
14169
+ path: assertion.path,
14170
+ ok: true,
14171
+ exists: resolved.exists,
14172
+ observed_type: resolved.exists ? jsonProbeValueType(resolved.value) : "missing",
14173
+ };
14174
+ if (resolved.exists) attachJsonProbeObservedValue(result, resolved.value);
14175
+ if (resolved.error) errors.push(resolved.error);
14176
+ if (Object.hasOwn(assertion, "exists")) {
14177
+ result.expected_exists = assertion.exists;
14178
+ if (resolved.exists !== assertion.exists) errors.push("expected exists=" + assertion.exists);
14179
+ }
14180
+ if (Object.hasOwn(assertion, "type")) {
14181
+ result.type = assertion.type;
14182
+ if (!resolved.exists || jsonProbeValueType(resolved.value) !== assertion.type) errors.push("expected type " + assertion.type);
14183
+ }
14184
+ if (Object.hasOwn(assertion, "equals")) {
14185
+ result.equals = assertion.equals;
14186
+ if (!resolved.exists || !jsonProbeDeepEqual(resolved.value, assertion.equals)) errors.push("expected JSON value equality");
14187
+ }
14188
+ if (Object.hasOwn(assertion, "not_equals")) {
14189
+ result.not_equals = assertion.not_equals;
14190
+ if (resolved.exists && jsonProbeDeepEqual(resolved.value, assertion.not_equals)) errors.push("expected JSON value inequality");
14191
+ }
14192
+ if (Object.hasOwn(assertion, "contains")) {
14193
+ result.contains = assertion.contains;
14194
+ if (!resolved.exists || !jsonProbeContains(resolved.value, assertion.contains)) errors.push("expected JSON value containment");
14195
+ }
14196
+ result.ok = errors.length === 0;
14197
+ if (errors.length) result.errors = errors;
14198
+ return result;
14199
+ }
14200
+ function evaluateJsonProbeAssertions(text, assertions) {
14201
+ const expected = Array.isArray(assertions) ? assertions.filter((assertion) => assertion && assertion.path) : [];
14202
+ if (!expected.length) return [];
14203
+ let parsed;
14204
+ try {
14205
+ parsed = JSON.parse(text);
14206
+ } catch (error) {
14207
+ const message = "response body is not valid JSON: " + String(error && error.message ? error.message : error).slice(0, 200);
14208
+ return expected.map((assertion) => ({
14209
+ label: assertion.label || assertion.path,
14210
+ path: assertion.path,
14211
+ ok: false,
14212
+ exists: false,
14213
+ observed_type: "missing",
14214
+ errors: [message],
14215
+ }));
14216
+ }
14217
+ return expected.map((assertion) => evaluateJsonProbeAssertion(parsed, assertion));
14218
+ }
13724
14219
  async function collectHttpStatus(check) {
13725
14220
  const url = httpStatusRequestUrl(check, page.url() || targetUrl);
13726
14221
  const method = httpStatusMethod(check);
@@ -13737,6 +14232,7 @@ async function collectHttpStatus(check) {
13737
14232
  const bodyContains = Array.isArray(check.body_contains) ? check.body_contains.filter(Boolean) : [];
13738
14233
  const bodyNotContains = Array.isArray(check.body_not_contains) ? check.body_not_contains.filter(Boolean) : [];
13739
14234
  const bodyNotPatterns = Array.isArray(check.body_not_patterns) ? check.body_not_patterns.filter(Boolean) : [];
14235
+ const bodyJsonAssertions = Array.isArray(check.body_json_assertions) ? check.body_json_assertions.filter((assertion) => assertion && assertion.path) : [];
13740
14236
  const options = {
13741
14237
  method,
13742
14238
  redirect: "follow",
@@ -13762,17 +14258,18 @@ async function collectHttpStatus(check) {
13762
14258
  Object.assign(result, linkProbeResponseFields(response, method));
13763
14259
  result.url = url;
13764
14260
  result.status_text = response.statusText || "";
13765
- const shouldReadBody = check.require_nonzero_bytes === true || (typeof check.min_bytes === "number" && Number.isFinite(check.min_bytes)) || bodyContains.length > 0 || bodyNotContains.length > 0 || bodyNotPatterns.length > 0;
14261
+ const shouldReadBody = check.require_nonzero_bytes === true || (typeof check.min_bytes === "number" && Number.isFinite(check.min_bytes)) || bodyContains.length > 0 || bodyNotContains.length > 0 || bodyNotPatterns.length > 0 || bodyJsonAssertions.length > 0;
13766
14262
  if (shouldReadBody) {
13767
14263
  try {
13768
14264
  const buffer = await response.arrayBuffer();
13769
14265
  result.bytes = buffer.byteLength;
13770
- if (bodyContains.length || bodyNotContains.length || bodyNotPatterns.length) {
14266
+ if (bodyContains.length || bodyNotContains.length || bodyNotPatterns.length || bodyJsonAssertions.length) {
13771
14267
  const text = new TextDecoder().decode(buffer);
13772
14268
  result.body_sample = text.slice(0, 1000);
13773
14269
  if (bodyContains.length) result.body_contains = Object.fromEntries(bodyContains.map((expected) => [expected, text.includes(expected)]));
13774
14270
  if (bodyNotContains.length) result.body_not_contains = Object.fromEntries(bodyNotContains.map((forbidden) => [forbidden, text.includes(forbidden)]));
13775
14271
  if (bodyNotPatterns.length) result.body_not_patterns = Object.fromEntries(bodyNotPatterns.map((pattern) => [pattern, new RegExp(pattern).test(text)]));
14272
+ if (bodyJsonAssertions.length) result.body_json_assertions = evaluateJsonProbeAssertions(text, bodyJsonAssertions);
13776
14273
  }
13777
14274
  } catch (error) {
13778
14275
  result.error = String(error && error.message ? error.message : error).slice(0, 500);
@@ -13785,6 +14282,7 @@ async function collectHttpStatus(check) {
13785
14282
  && (!bodyContains.length || bodyContains.every((expected) => result.body_contains && result.body_contains[expected] === true))
13786
14283
  && (!bodyNotContains.length || bodyNotContains.every((forbidden) => result.body_not_contains && result.body_not_contains[forbidden] === false))
13787
14284
  && (!bodyNotPatterns.length || bodyNotPatterns.every((pattern) => result.body_not_patterns && result.body_not_patterns[pattern] === false))
14285
+ && (!bodyJsonAssertions.length || (Array.isArray(result.body_json_assertions) && result.body_json_assertions.every((assertion) => assertion.ok === true)))
13788
14286
  && !result.error;
13789
14287
  return result;
13790
14288
  } catch (error) {