@riddledc/riddle-proof 0.7.126 → 0.7.127

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/cli.cjs CHANGED
@@ -7076,6 +7076,9 @@ function valueFromOwn(input, ...keys) {
7076
7076
  function numberValue(value) {
7077
7077
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
7078
7078
  }
7079
+ function booleanValue(value) {
7080
+ return typeof value === "boolean" ? value : void 0;
7081
+ }
7079
7082
  function horizontalBoundsOverflowPx(value) {
7080
7083
  if (!isRecord(value)) return 0;
7081
7084
  let max = maxPositiveNumber(
@@ -7170,6 +7173,156 @@ function toJsonValue(value) {
7170
7173
  if (isRecord(value)) return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, toJsonValue(child)]));
7171
7174
  return String(value);
7172
7175
  }
7176
+ function jsonValueType(value) {
7177
+ if (value === null) return "null";
7178
+ if (Array.isArray(value)) return "array";
7179
+ if (typeof value === "boolean") return "boolean";
7180
+ if (typeof value === "number") return "number";
7181
+ if (typeof value === "string") return "string";
7182
+ return "object";
7183
+ }
7184
+ function deepJsonEqual(left, right) {
7185
+ if (left === right) return true;
7186
+ if (typeof left !== typeof right) return false;
7187
+ if (left === null || right === null) return left === right;
7188
+ if (typeof left !== "object" || typeof right !== "object") return false;
7189
+ if (Array.isArray(left) || Array.isArray(right)) {
7190
+ if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) return false;
7191
+ return left.every((item, index) => deepJsonEqual(item, right[index]));
7192
+ }
7193
+ if (!isRecord(left) || !isRecord(right)) return false;
7194
+ const leftKeys = Object.keys(left).sort();
7195
+ const rightKeys = Object.keys(right).sort();
7196
+ if (!deepJsonEqual(leftKeys, rightKeys)) return false;
7197
+ return leftKeys.every((key) => deepJsonEqual(left[key], right[key]));
7198
+ }
7199
+ function jsonContains(observed, expected) {
7200
+ if (typeof observed === "string" && typeof expected === "string") {
7201
+ return observed.includes(expected);
7202
+ }
7203
+ if (Array.isArray(observed)) {
7204
+ return observed.some((item) => deepJsonEqual(item, expected));
7205
+ }
7206
+ if (isRecord(observed) && isRecord(expected)) {
7207
+ return Object.entries(expected).every(([key, value]) => hasOwn(observed, key) && deepJsonEqual(observed[key], value));
7208
+ }
7209
+ return false;
7210
+ }
7211
+ function parseJsonPathSegments(path7) {
7212
+ let input = path7.trim();
7213
+ if (!input) throw new Error("path is empty");
7214
+ if (input === "$") return [];
7215
+ if (input.startsWith("$.")) input = input.slice(2);
7216
+ else if (input.startsWith("$[")) input = input.slice(1);
7217
+ const segments = [];
7218
+ let token = "";
7219
+ const pushToken = () => {
7220
+ if (!token) return;
7221
+ segments.push(token);
7222
+ token = "";
7223
+ };
7224
+ for (let index = 0; index < input.length; index += 1) {
7225
+ const char = input[index];
7226
+ if (char === ".") {
7227
+ pushToken();
7228
+ continue;
7229
+ }
7230
+ if (char !== "[") {
7231
+ token += char;
7232
+ continue;
7233
+ }
7234
+ pushToken();
7235
+ const closeIndex = input.indexOf("]", index + 1);
7236
+ if (closeIndex === -1) throw new Error(`unterminated bracket at ${index}`);
7237
+ const bracket = input.slice(index + 1, closeIndex).trim();
7238
+ if (!bracket) throw new Error(`empty bracket at ${index}`);
7239
+ if (/^\d+$/.test(bracket)) {
7240
+ segments.push(Number(bracket));
7241
+ } else if (bracket.startsWith('"') && bracket.endsWith('"') || bracket.startsWith("'") && bracket.endsWith("'")) {
7242
+ const quoted = bracket.startsWith("'") ? `"${bracket.slice(1, -1).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"` : bracket;
7243
+ segments.push(String(JSON.parse(quoted)));
7244
+ } else {
7245
+ segments.push(bracket);
7246
+ }
7247
+ index = closeIndex;
7248
+ }
7249
+ pushToken();
7250
+ return segments;
7251
+ }
7252
+ function resolveJsonPath(root, path7) {
7253
+ let segments;
7254
+ try {
7255
+ segments = parseJsonPathSegments(path7);
7256
+ } catch (error) {
7257
+ return { exists: false, error: String(error instanceof Error ? error.message : error) };
7258
+ }
7259
+ let current = root;
7260
+ for (const segment of segments) {
7261
+ if (typeof segment === "number") {
7262
+ if (!Array.isArray(current) || segment < 0 || segment >= current.length) return { exists: false };
7263
+ current = current[segment];
7264
+ continue;
7265
+ }
7266
+ if (!isRecord(current) || !hasOwn(current, segment)) return { exists: false };
7267
+ current = current[segment];
7268
+ }
7269
+ return { exists: true, value: current };
7270
+ }
7271
+ function evaluateHttpStatusBodyJsonAssertion(root, assertion) {
7272
+ const resolved = resolveJsonPath(root, assertion.path);
7273
+ const errors = [];
7274
+ const result = {
7275
+ label: assertion.label || assertion.path,
7276
+ path: assertion.path,
7277
+ ok: true,
7278
+ exists: resolved.exists,
7279
+ observed_type: resolved.exists ? jsonValueType(resolved.value) : "missing"
7280
+ };
7281
+ if (resolved.exists) result.observed = toJsonValue(resolved.value);
7282
+ if (resolved.error) errors.push(resolved.error);
7283
+ if (hasOwn(assertion, "exists")) {
7284
+ result.expected_exists = assertion.exists;
7285
+ if (resolved.exists !== assertion.exists) errors.push(`expected exists=${assertion.exists}`);
7286
+ }
7287
+ if (hasOwn(assertion, "type")) {
7288
+ result.type = assertion.type;
7289
+ if (!resolved.exists || jsonValueType(resolved.value) !== assertion.type) errors.push(`expected type ${assertion.type}`);
7290
+ }
7291
+ if (hasOwn(assertion, "equals")) {
7292
+ result.equals = assertion.equals;
7293
+ if (!resolved.exists || !deepJsonEqual(resolved.value, assertion.equals)) errors.push("expected JSON value equality");
7294
+ }
7295
+ if (hasOwn(assertion, "not_equals")) {
7296
+ result.not_equals = assertion.not_equals;
7297
+ if (resolved.exists && deepJsonEqual(resolved.value, assertion.not_equals)) errors.push("expected JSON value inequality");
7298
+ }
7299
+ if (hasOwn(assertion, "contains")) {
7300
+ result.contains = assertion.contains;
7301
+ if (!resolved.exists || !jsonContains(resolved.value, assertion.contains)) errors.push("expected JSON value containment");
7302
+ }
7303
+ result.ok = errors.length === 0;
7304
+ if (errors.length) result.errors = errors;
7305
+ return result;
7306
+ }
7307
+ function evaluateHttpStatusBodyJsonAssertions(bodyText, assertions) {
7308
+ const expected = assertions?.filter((assertion) => assertion.path) ?? [];
7309
+ if (!expected.length) return [];
7310
+ let parsed;
7311
+ try {
7312
+ parsed = JSON.parse(bodyText);
7313
+ } catch (error) {
7314
+ const message = `response body is not valid JSON: ${String(error instanceof Error ? error.message : error).slice(0, 200)}`;
7315
+ return expected.map((assertion) => ({
7316
+ label: assertion.label || assertion.path,
7317
+ path: assertion.path,
7318
+ ok: false,
7319
+ exists: false,
7320
+ observed_type: "missing",
7321
+ errors: [message]
7322
+ }));
7323
+ }
7324
+ return expected.map((assertion) => evaluateHttpStatusBodyJsonAssertion(parsed, assertion));
7325
+ }
7173
7326
  function compactProfileSetupSummaryText(value, limit = 160) {
7174
7327
  const text = typeof value === "string" ? value.replace(/\s+/g, " ").trim() : "";
7175
7328
  if (!text) return void 0;
@@ -7824,6 +7977,46 @@ function validateRegexPatterns(patterns, label) {
7824
7977
  }
7825
7978
  }
7826
7979
  }
7980
+ function normalizeHttpStatusBodyJsonAssertions(value, label) {
7981
+ if (value === void 0) return void 0;
7982
+ if (!Array.isArray(value)) throw new Error(`${label} must be an array.`);
7983
+ if (!value.length) throw new Error(`${label} must not be empty.`);
7984
+ return value.map((item, index) => {
7985
+ const itemLabel = `${label}[${index}]`;
7986
+ if (typeof item === "string") {
7987
+ const path8 = stringValue2(item);
7988
+ if (!path8) throw new Error(`${itemLabel} path must not be empty.`);
7989
+ return { path: path8, exists: true };
7990
+ }
7991
+ if (!isRecord(item)) throw new Error(`${itemLabel} must be an object or JSON path string.`);
7992
+ const path7 = stringFromOwn(item, "path", "json_path", "jsonPath", "key");
7993
+ if (!path7) throw new Error(`${itemLabel}.path is required.`);
7994
+ const assertion = {
7995
+ label: stringValue2(item.label),
7996
+ path: path7
7997
+ };
7998
+ const exists = booleanValue(valueFromOwn(item, "exists", "present"));
7999
+ if (exists !== void 0) assertion.exists = exists;
8000
+ const type = stringValue2(valueFromOwn(item, "type", "value_type", "valueType"));
8001
+ if (type !== void 0) {
8002
+ const allowedTypes = ["array", "boolean", "null", "number", "object", "string"];
8003
+ if (!allowedTypes.includes(type)) {
8004
+ throw new Error(`${itemLabel}.type must be one of ${allowedTypes.join(", ")}.`);
8005
+ }
8006
+ assertion.type = type;
8007
+ }
8008
+ const equalsValue = valueFromOwn(item, "equals", "expected", "expected_value", "expectedValue", "value");
8009
+ if (equalsValue !== void 0) assertion.equals = toJsonValue(equalsValue);
8010
+ const notEqualsValue = valueFromOwn(item, "not_equals", "notEquals", "forbidden", "forbidden_value", "forbiddenValue");
8011
+ if (notEqualsValue !== void 0) assertion.not_equals = toJsonValue(notEqualsValue);
8012
+ const containsValue = valueFromOwn(item, "contains", "includes", "contains_value", "containsValue", "include");
8013
+ if (containsValue !== void 0) assertion.contains = toJsonValue(containsValue);
8014
+ if (assertion.exists === void 0 && assertion.type === void 0 && !hasOwn(assertion, "equals") && !hasOwn(assertion, "not_equals") && !hasOwn(assertion, "contains")) {
8015
+ assertion.exists = true;
8016
+ }
8017
+ return assertion;
8018
+ });
8019
+ }
7827
8020
  function isDialogCountCheckType(type) {
7828
8021
  return type === "dialog_count_equals" || type === "dialog_accept_count_equals" || type === "dialog_dismiss_count_equals";
7829
8022
  }
@@ -7923,6 +8116,10 @@ function normalizeCheck(input, index) {
7923
8116
  `checks[${index}] body_not_patterns`
7924
8117
  ) : void 0;
7925
8118
  if (bodyNotPatterns?.length) validateRegexPatterns(bodyNotPatterns, `checks[${index}] body_not_patterns`);
8119
+ const bodyJsonAssertions = isHttpStatusCheck ? normalizeHttpStatusBodyJsonAssertions(
8120
+ input.body_json_assertions ?? input.bodyJsonAssertions ?? input.json_body_assertions ?? input.jsonBodyAssertions ?? input.json_assertions ?? input.jsonAssertions ?? input.response_json_assertions ?? input.responseJsonAssertions,
8121
+ `checks[${index}] body_json_assertions`
8122
+ ) : void 0;
7926
8123
  if (isLinkStatusCheck) {
7927
8124
  if (minCount !== void 0 && (!Number.isInteger(minCount) || minCount < 0)) {
7928
8125
  throw new Error(`checks[${index}] ${type} min_count must be a non-negative integer.`);
@@ -7948,6 +8145,7 @@ function normalizeCheck(input, index) {
7948
8145
  body_contains: bodyContains,
7949
8146
  body_not_contains: bodyNotContains,
7950
8147
  body_not_patterns: bodyNotPatterns,
8148
+ body_json_assertions: bodyJsonAssertions,
7951
8149
  expected_texts: expectedTexts,
7952
8150
  link_selector: stringValue2(input.link_selector) || stringValue2(input.linkSelector),
7953
8151
  source_selector: stringValue2(input.source_selector) || stringValue2(input.sourceSelector),
@@ -8154,6 +8352,34 @@ function httpStatusBodyNotPatternFailures(result, check) {
8154
8352
  const observed = isRecord(result.body_not_patterns) ? result.body_not_patterns : {};
8155
8353
  return forbidden.filter((pattern) => observed[pattern] !== false);
8156
8354
  }
8355
+ function httpStatusBodyJsonAssertionFailures(result, check) {
8356
+ const expected = check.body_json_assertions?.filter((assertion) => assertion.path) ?? [];
8357
+ if (!expected.length) return [];
8358
+ if (!Array.isArray(result.body_json_assertions)) {
8359
+ return expected.map((assertion) => ({
8360
+ label: assertion.label || assertion.path,
8361
+ path: assertion.path,
8362
+ ok: false,
8363
+ exists: false,
8364
+ observed_type: "missing",
8365
+ errors: ["body_json_assertions evidence missing"]
8366
+ }));
8367
+ }
8368
+ return result.body_json_assertions.filter((assertion) => isRecord(assertion) && assertion.ok !== true).map((assertion) => ({
8369
+ label: stringValue2(assertion.label) || stringValue2(assertion.path) || "json assertion",
8370
+ path: stringValue2(assertion.path) || "",
8371
+ ok: false,
8372
+ exists: assertion.exists === true,
8373
+ observed: hasOwn(assertion, "observed") ? toJsonValue(assertion.observed) : void 0,
8374
+ observed_type: stringValue2(assertion.observed_type) || "missing",
8375
+ expected_exists: booleanValue(assertion.expected_exists),
8376
+ equals: hasOwn(assertion, "equals") ? toJsonValue(assertion.equals) : void 0,
8377
+ not_equals: hasOwn(assertion, "not_equals") ? toJsonValue(assertion.not_equals) : void 0,
8378
+ contains: hasOwn(assertion, "contains") ? toJsonValue(assertion.contains) : void 0,
8379
+ type: stringValue2(assertion.type),
8380
+ errors: Array.isArray(assertion.errors) ? assertion.errors.map(String) : void 0
8381
+ }));
8382
+ }
8157
8383
  function linkStatusResultOk(result, check) {
8158
8384
  const status = numberValue(result.status);
8159
8385
  if (!httpStatusIsAllowed(status, check)) return false;
@@ -8171,6 +8397,7 @@ function linkStatusResultOk(result, check) {
8171
8397
  if (httpStatusBodyContainsFailures(result, check).length) return false;
8172
8398
  if (httpStatusBodyNotContainsFailures(result, check).length) return false;
8173
8399
  if (httpStatusBodyNotPatternFailures(result, check).length) return false;
8400
+ if (httpStatusBodyJsonAssertionFailures(result, check).length) return false;
8174
8401
  return true;
8175
8402
  }
8176
8403
  function responseHeader(response, name) {
@@ -8239,7 +8466,7 @@ async function preflightHttpStatusCheck(check, index, targetUrl, fetchImpl) {
8239
8466
  statusText = typeof response.statusText === "string" ? response.statusText : "";
8240
8467
  result.content_type = responseHeader(response, "content-type");
8241
8468
  result.content_length = responseContentLength(response);
8242
- 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);
8469
+ 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);
8243
8470
  if (shouldReadBody && method !== "HEAD") {
8244
8471
  const body = await responseBodyText(response);
8245
8472
  result.bytes = body.bytes;
@@ -8252,6 +8479,9 @@ async function preflightHttpStatusCheck(check, index, targetUrl, fetchImpl) {
8252
8479
  if (check.body_not_patterns?.length) {
8253
8480
  result.body_not_patterns = Object.fromEntries(check.body_not_patterns.filter(Boolean).map((pattern) => [pattern, new RegExp(pattern).test(body.text)]));
8254
8481
  }
8482
+ if (check.body_json_assertions?.length) {
8483
+ result.body_json_assertions = evaluateHttpStatusBodyJsonAssertions(body.text, check.body_json_assertions);
8484
+ }
8255
8485
  }
8256
8486
  } catch (caught) {
8257
8487
  error = String(caught instanceof Error ? caught.message : caught).slice(0, 500);
@@ -8260,6 +8490,7 @@ async function preflightHttpStatusCheck(check, index, targetUrl, fetchImpl) {
8260
8490
  const bodyContainsMissing = httpStatusBodyContainsFailures(result, check);
8261
8491
  const bodyNotContainsFound = httpStatusBodyNotContainsFailures(result, check);
8262
8492
  const bodyNotPatternsFound = httpStatusBodyNotPatternFailures(result, check);
8493
+ const bodyJsonAssertionsFailed = httpStatusBodyJsonAssertionFailures(result, check);
8263
8494
  const ok = !error && linkStatusResultOk(result, check);
8264
8495
  return {
8265
8496
  index,
@@ -8278,7 +8509,9 @@ async function preflightHttpStatusCheck(check, index, targetUrl, fetchImpl) {
8278
8509
  body_not_contains: isRecord(result.body_not_contains) ? Object.fromEntries(Object.entries(result.body_not_contains).map(([key, value]) => [key, value === true])) : null,
8279
8510
  body_not_contains_found: bodyNotContainsFound,
8280
8511
  body_not_patterns: isRecord(result.body_not_patterns) ? Object.fromEntries(Object.entries(result.body_not_patterns).map(([key, value]) => [key, value === true])) : null,
8281
- body_not_patterns_found: bodyNotPatternsFound
8512
+ body_not_patterns_found: bodyNotPatternsFound,
8513
+ body_json_assertions: Array.isArray(result.body_json_assertions) ? result.body_json_assertions : null,
8514
+ body_json_assertions_failed: bodyJsonAssertionsFailed
8282
8515
  };
8283
8516
  }
8284
8517
  async function preflightRiddleProofProfileHttpStatusChecks(profile, options = {}) {
@@ -8323,6 +8556,7 @@ function summarizeHttpStatusEvidence(viewport, check) {
8323
8556
  const bodyContainsMissing = httpStatusBodyContainsFailures(statusEvidence, check);
8324
8557
  const bodyNotContainsFound = httpStatusBodyNotContainsFailures(statusEvidence, check);
8325
8558
  const bodyNotPatternsFound = httpStatusBodyNotPatternFailures(statusEvidence, check);
8559
+ const bodyJsonAssertionsFailed = httpStatusBodyJsonAssertionFailures(statusEvidence, check);
8326
8560
  if (!linkStatusResultOk(statusEvidence, check)) {
8327
8561
  failures.push({
8328
8562
  code: "http_status_failed",
@@ -8341,6 +8575,8 @@ function summarizeHttpStatusEvidence(viewport, check) {
8341
8575
  body_not_contains_found: bodyNotContainsFound,
8342
8576
  body_not_patterns: check.body_not_patterns ?? null,
8343
8577
  body_not_patterns_found: bodyNotPatternsFound,
8578
+ body_json_assertions: check.body_json_assertions ?? null,
8579
+ body_json_assertions_failed: bodyJsonAssertionsFailed.map((assertion) => toJsonValue(assertion)),
8344
8580
  body_sample: stringValue2(statusEvidence.body_sample) ?? null
8345
8581
  });
8346
8582
  }
@@ -8362,6 +8598,8 @@ function summarizeHttpStatusEvidence(viewport, check) {
8362
8598
  body_not_contains_found: bodyNotContainsFound,
8363
8599
  body_not_patterns: isRecord(statusEvidence.body_not_patterns) ? toJsonValue(statusEvidence.body_not_patterns) : null,
8364
8600
  body_not_patterns_found: bodyNotPatternsFound,
8601
+ body_json_assertions: Array.isArray(statusEvidence.body_json_assertions) ? toJsonValue(statusEvidence.body_json_assertions) : null,
8602
+ body_json_assertions_failed: bodyJsonAssertionsFailed.map((assertion) => toJsonValue(assertion)),
8365
8603
  body_sample: stringValue2(statusEvidence.body_sample) ?? null,
8366
8604
  failures
8367
8605
  };
@@ -8998,6 +9236,7 @@ function assessCheckFromEvidence(check, evidence) {
8998
9236
  body_contains: check.body_contains ?? [],
8999
9237
  body_not_contains: check.body_not_contains ?? [],
9000
9238
  body_not_patterns: check.body_not_patterns ?? [],
9239
+ body_json_assertions: toJsonValue(check.body_json_assertions ?? []),
9001
9240
  viewports: summaries.map((summary) => toJsonValue(summary)),
9002
9241
  failures: failed.flatMap((summary) => Array.isArray(summary.failures) ? summary.failures.map((failure) => toJsonValue({ viewport: stringValue2(summary.viewport) ?? null, failure })) : [])
9003
9242
  },
@@ -9725,6 +9964,36 @@ function httpStatusBodyNotPatternFailures(result, check) {
9725
9964
  : {};
9726
9965
  return forbidden.filter((pattern) => observed[pattern] !== false);
9727
9966
  }
9967
+ function httpStatusBodyJsonAssertionFailures(result, check) {
9968
+ const expected = Array.isArray(check.body_json_assertions) ? check.body_json_assertions.filter((assertion) => assertion && assertion.path) : [];
9969
+ if (!expected.length) return [];
9970
+ if (!Array.isArray(result.body_json_assertions)) {
9971
+ return expected.map((assertion) => ({
9972
+ label: assertion.label || assertion.path,
9973
+ path: assertion.path,
9974
+ ok: false,
9975
+ exists: false,
9976
+ observed_type: "missing",
9977
+ errors: ["body_json_assertions evidence missing"],
9978
+ }));
9979
+ }
9980
+ return result.body_json_assertions
9981
+ .filter((assertion) => assertion && typeof assertion === "object" && assertion.ok !== true)
9982
+ .map((assertion) => ({
9983
+ label: typeof assertion.label === "string" && assertion.label ? assertion.label : typeof assertion.path === "string" && assertion.path ? assertion.path : "json assertion",
9984
+ path: typeof assertion.path === "string" ? assertion.path : "",
9985
+ ok: false,
9986
+ exists: assertion.exists === true,
9987
+ observed: Object.hasOwn(assertion, "observed") ? assertion.observed : undefined,
9988
+ observed_type: typeof assertion.observed_type === "string" && assertion.observed_type ? assertion.observed_type : "missing",
9989
+ expected_exists: typeof assertion.expected_exists === "boolean" ? assertion.expected_exists : undefined,
9990
+ equals: Object.hasOwn(assertion, "equals") ? assertion.equals : undefined,
9991
+ not_equals: Object.hasOwn(assertion, "not_equals") ? assertion.not_equals : undefined,
9992
+ contains: Object.hasOwn(assertion, "contains") ? assertion.contains : undefined,
9993
+ type: typeof assertion.type === "string" ? assertion.type : undefined,
9994
+ errors: Array.isArray(assertion.errors) ? assertion.errors.map(String) : undefined,
9995
+ }));
9996
+ }
9728
9997
  function linkStatusResultOk(result, check) {
9729
9998
  if (!result || typeof result !== "object" || Array.isArray(result)) return false;
9730
9999
  if (!httpStatusIsAllowed(result.status, check)) return false;
@@ -9742,6 +10011,7 @@ function linkStatusResultOk(result, check) {
9742
10011
  if (httpStatusBodyContainsFailures(result, check).length) return false;
9743
10012
  if (httpStatusBodyNotContainsFailures(result, check).length) return false;
9744
10013
  if (httpStatusBodyNotPatternFailures(result, check).length) return false;
10014
+ if (httpStatusBodyJsonAssertionFailures(result, check).length) return false;
9745
10015
  return true;
9746
10016
  }
9747
10017
  function summarizeHttpStatusEvidence(viewport, check) {
@@ -9762,6 +10032,7 @@ function summarizeHttpStatusEvidence(viewport, check) {
9762
10032
  const bodyContainsMissing = httpStatusBodyContainsFailures(statusEvidence, check);
9763
10033
  const bodyNotContainsFound = httpStatusBodyNotContainsFailures(statusEvidence, check);
9764
10034
  const bodyNotPatternsFound = httpStatusBodyNotPatternFailures(statusEvidence, check);
10035
+ const bodyJsonAssertionsFailed = httpStatusBodyJsonAssertionFailures(statusEvidence, check);
9765
10036
  if (!linkStatusResultOk(statusEvidence, check)) {
9766
10037
  failures.push({
9767
10038
  code: "http_status_failed",
@@ -9780,6 +10051,8 @@ function summarizeHttpStatusEvidence(viewport, check) {
9780
10051
  body_not_contains_found: bodyNotContainsFound,
9781
10052
  body_not_patterns: Array.isArray(check.body_not_patterns) ? check.body_not_patterns : null,
9782
10053
  body_not_patterns_found: bodyNotPatternsFound,
10054
+ body_json_assertions: Array.isArray(check.body_json_assertions) ? check.body_json_assertions : null,
10055
+ body_json_assertions_failed: bodyJsonAssertionsFailed,
9783
10056
  body_sample: typeof statusEvidence.body_sample === "string" ? statusEvidence.body_sample : null,
9784
10057
  });
9785
10058
  }
@@ -9807,6 +10080,8 @@ function summarizeHttpStatusEvidence(viewport, check) {
9807
10080
  ? statusEvidence.body_not_patterns
9808
10081
  : null,
9809
10082
  body_not_patterns_found: bodyNotPatternsFound,
10083
+ body_json_assertions: Array.isArray(statusEvidence.body_json_assertions) ? statusEvidence.body_json_assertions : null,
10084
+ body_json_assertions_failed: bodyJsonAssertionsFailed,
9810
10085
  body_sample: typeof statusEvidence.body_sample === "string" ? statusEvidence.body_sample : null,
9811
10086
  failures,
9812
10087
  };
@@ -11909,6 +12184,155 @@ function linkProbeResponseFields(response, method) {
11909
12184
  content_length: contentLength,
11910
12185
  };
11911
12186
  }
12187
+ function jsonProbeValueType(value) {
12188
+ if (value === null) return "null";
12189
+ if (Array.isArray(value)) return "array";
12190
+ if (typeof value === "boolean") return "boolean";
12191
+ if (typeof value === "number") return "number";
12192
+ if (typeof value === "string") return "string";
12193
+ return "object";
12194
+ }
12195
+ function jsonProbeDeepEqual(left, right) {
12196
+ if (left === right) return true;
12197
+ if (typeof left !== typeof right) return false;
12198
+ if (left === null || right === null) return left === right;
12199
+ if (typeof left !== "object" || typeof right !== "object") return false;
12200
+ if (Array.isArray(left) || Array.isArray(right)) {
12201
+ if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) return false;
12202
+ return left.every((item, index) => jsonProbeDeepEqual(item, right[index]));
12203
+ }
12204
+ const leftKeys = Object.keys(left).sort();
12205
+ const rightKeys = Object.keys(right).sort();
12206
+ if (!jsonProbeDeepEqual(leftKeys, rightKeys)) return false;
12207
+ return leftKeys.every((key) => jsonProbeDeepEqual(left[key], right[key]));
12208
+ }
12209
+ function jsonProbeContains(observed, expected) {
12210
+ if (typeof observed === "string" && typeof expected === "string") return observed.includes(expected);
12211
+ if (Array.isArray(observed)) return observed.some((item) => jsonProbeDeepEqual(item, expected));
12212
+ if (observed && expected && typeof observed === "object" && typeof expected === "object" && !Array.isArray(observed) && !Array.isArray(expected)) {
12213
+ return Object.entries(expected).every(([key, value]) => Object.hasOwn(observed, key) && jsonProbeDeepEqual(observed[key], value));
12214
+ }
12215
+ return false;
12216
+ }
12217
+ function parseJsonProbePathSegments(path) {
12218
+ let input = String(path || "").trim();
12219
+ if (!input) throw new Error("path is empty");
12220
+ if (input === "$") return [];
12221
+ if (input.startsWith("$.")) input = input.slice(2);
12222
+ else if (input.startsWith("$[")) input = input.slice(1);
12223
+ const segments = [];
12224
+ let token = "";
12225
+ const pushToken = () => {
12226
+ if (!token) return;
12227
+ segments.push(token);
12228
+ token = "";
12229
+ };
12230
+ for (let index = 0; index < input.length; index += 1) {
12231
+ const char = input[index];
12232
+ if (char === ".") {
12233
+ pushToken();
12234
+ continue;
12235
+ }
12236
+ if (char !== "[") {
12237
+ token += char;
12238
+ continue;
12239
+ }
12240
+ pushToken();
12241
+ const closeIndex = input.indexOf("]", index + 1);
12242
+ if (closeIndex === -1) throw new Error("unterminated bracket at " + index);
12243
+ const bracket = input.slice(index + 1, closeIndex).trim();
12244
+ if (!bracket) throw new Error("empty bracket at " + index);
12245
+ if (/^\d+$/.test(bracket)) {
12246
+ segments.push(Number(bracket));
12247
+ } else if ((bracket.startsWith('"') && bracket.endsWith('"')) || (bracket.startsWith("'") && bracket.endsWith("'"))) {
12248
+ const quoted = bracket.startsWith("'")
12249
+ ? '"' + bracket.slice(1, -1).replace(/\\/g, "\\\\").replace(/"/g, "\\\"") + '"'
12250
+ : bracket;
12251
+ segments.push(String(JSON.parse(quoted)));
12252
+ } else {
12253
+ segments.push(bracket);
12254
+ }
12255
+ index = closeIndex;
12256
+ }
12257
+ pushToken();
12258
+ return segments;
12259
+ }
12260
+ function resolveJsonProbePath(root, path) {
12261
+ let segments;
12262
+ try {
12263
+ segments = parseJsonProbePathSegments(path);
12264
+ } catch (error) {
12265
+ return { exists: false, error: String(error && error.message ? error.message : error) };
12266
+ }
12267
+ let current = root;
12268
+ for (const segment of segments) {
12269
+ if (typeof segment === "number") {
12270
+ if (!Array.isArray(current) || segment < 0 || segment >= current.length) return { exists: false };
12271
+ current = current[segment];
12272
+ continue;
12273
+ }
12274
+ if (!current || typeof current !== "object" || Array.isArray(current) || !Object.hasOwn(current, segment)) {
12275
+ return { exists: false };
12276
+ }
12277
+ current = current[segment];
12278
+ }
12279
+ return { exists: true, value: current };
12280
+ }
12281
+ function evaluateJsonProbeAssertion(root, assertion) {
12282
+ const resolved = resolveJsonProbePath(root, assertion.path);
12283
+ const errors = [];
12284
+ const result = {
12285
+ label: assertion.label || assertion.path,
12286
+ path: assertion.path,
12287
+ ok: true,
12288
+ exists: resolved.exists,
12289
+ observed_type: resolved.exists ? jsonProbeValueType(resolved.value) : "missing",
12290
+ };
12291
+ if (resolved.exists) result.observed = resolved.value;
12292
+ if (resolved.error) errors.push(resolved.error);
12293
+ if (Object.hasOwn(assertion, "exists")) {
12294
+ result.expected_exists = assertion.exists;
12295
+ if (resolved.exists !== assertion.exists) errors.push("expected exists=" + assertion.exists);
12296
+ }
12297
+ if (Object.hasOwn(assertion, "type")) {
12298
+ result.type = assertion.type;
12299
+ if (!resolved.exists || jsonProbeValueType(resolved.value) !== assertion.type) errors.push("expected type " + assertion.type);
12300
+ }
12301
+ if (Object.hasOwn(assertion, "equals")) {
12302
+ result.equals = assertion.equals;
12303
+ if (!resolved.exists || !jsonProbeDeepEqual(resolved.value, assertion.equals)) errors.push("expected JSON value equality");
12304
+ }
12305
+ if (Object.hasOwn(assertion, "not_equals")) {
12306
+ result.not_equals = assertion.not_equals;
12307
+ if (resolved.exists && jsonProbeDeepEqual(resolved.value, assertion.not_equals)) errors.push("expected JSON value inequality");
12308
+ }
12309
+ if (Object.hasOwn(assertion, "contains")) {
12310
+ result.contains = assertion.contains;
12311
+ if (!resolved.exists || !jsonProbeContains(resolved.value, assertion.contains)) errors.push("expected JSON value containment");
12312
+ }
12313
+ result.ok = errors.length === 0;
12314
+ if (errors.length) result.errors = errors;
12315
+ return result;
12316
+ }
12317
+ function evaluateJsonProbeAssertions(text, assertions) {
12318
+ const expected = Array.isArray(assertions) ? assertions.filter((assertion) => assertion && assertion.path) : [];
12319
+ if (!expected.length) return [];
12320
+ let parsed;
12321
+ try {
12322
+ parsed = JSON.parse(text);
12323
+ } catch (error) {
12324
+ const message = "response body is not valid JSON: " + String(error && error.message ? error.message : error).slice(0, 200);
12325
+ return expected.map((assertion) => ({
12326
+ label: assertion.label || assertion.path,
12327
+ path: assertion.path,
12328
+ ok: false,
12329
+ exists: false,
12330
+ observed_type: "missing",
12331
+ errors: [message],
12332
+ }));
12333
+ }
12334
+ return expected.map((assertion) => evaluateJsonProbeAssertion(parsed, assertion));
12335
+ }
11912
12336
  async function collectHttpStatus(check) {
11913
12337
  const url = httpStatusRequestUrl(check, page.url() || targetUrl);
11914
12338
  const method = httpStatusMethod(check);
@@ -11925,6 +12349,7 @@ async function collectHttpStatus(check) {
11925
12349
  const bodyContains = Array.isArray(check.body_contains) ? check.body_contains.filter(Boolean) : [];
11926
12350
  const bodyNotContains = Array.isArray(check.body_not_contains) ? check.body_not_contains.filter(Boolean) : [];
11927
12351
  const bodyNotPatterns = Array.isArray(check.body_not_patterns) ? check.body_not_patterns.filter(Boolean) : [];
12352
+ const bodyJsonAssertions = Array.isArray(check.body_json_assertions) ? check.body_json_assertions.filter((assertion) => assertion && assertion.path) : [];
11928
12353
  const options = {
11929
12354
  method,
11930
12355
  redirect: "follow",
@@ -11950,17 +12375,18 @@ async function collectHttpStatus(check) {
11950
12375
  Object.assign(result, linkProbeResponseFields(response, method));
11951
12376
  result.url = url;
11952
12377
  result.status_text = response.statusText || "";
11953
- 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;
12378
+ 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;
11954
12379
  if (shouldReadBody) {
11955
12380
  try {
11956
12381
  const buffer = await response.arrayBuffer();
11957
12382
  result.bytes = buffer.byteLength;
11958
- if (bodyContains.length || bodyNotContains.length || bodyNotPatterns.length) {
12383
+ if (bodyContains.length || bodyNotContains.length || bodyNotPatterns.length || bodyJsonAssertions.length) {
11959
12384
  const text = new TextDecoder().decode(buffer);
11960
12385
  result.body_sample = text.slice(0, 1000);
11961
12386
  if (bodyContains.length) result.body_contains = Object.fromEntries(bodyContains.map((expected) => [expected, text.includes(expected)]));
11962
12387
  if (bodyNotContains.length) result.body_not_contains = Object.fromEntries(bodyNotContains.map((forbidden) => [forbidden, text.includes(forbidden)]));
11963
12388
  if (bodyNotPatterns.length) result.body_not_patterns = Object.fromEntries(bodyNotPatterns.map((pattern) => [pattern, new RegExp(pattern).test(text)]));
12389
+ if (bodyJsonAssertions.length) result.body_json_assertions = evaluateJsonProbeAssertions(text, bodyJsonAssertions);
11964
12390
  }
11965
12391
  } catch (error) {
11966
12392
  result.error = String(error && error.message ? error.message : error).slice(0, 500);
@@ -11973,6 +12399,7 @@ async function collectHttpStatus(check) {
11973
12399
  && (!bodyContains.length || bodyContains.every((expected) => result.body_contains && result.body_contains[expected] === true))
11974
12400
  && (!bodyNotContains.length || bodyNotContains.every((forbidden) => result.body_not_contains && result.body_not_contains[forbidden] === false))
11975
12401
  && (!bodyNotPatterns.length || bodyNotPatterns.every((pattern) => result.body_not_patterns && result.body_not_patterns[pattern] === false))
12402
+ && (!bodyJsonAssertions.length || (Array.isArray(result.body_json_assertions) && result.body_json_assertions.every((assertion) => assertion.ok === true)))
11976
12403
  && !result.error;
11977
12404
  return result;
11978
12405
  } catch (error) {
@@ -13685,6 +14112,21 @@ function profileHttpStatusAssertionKeys(evidence, viewports, field) {
13685
14112
  }
13686
14113
  return [...keys];
13687
14114
  }
14115
+ function profileHttpStatusJsonAssertionCount(viewports) {
14116
+ if (!viewports.length) return void 0;
14117
+ let passed = 0;
14118
+ let total = 0;
14119
+ for (const viewport of viewports) {
14120
+ if (!Array.isArray(viewport.body_json_assertions)) continue;
14121
+ for (const assertion of viewport.body_json_assertions) {
14122
+ const record = cliRecord(assertion);
14123
+ if (!record) continue;
14124
+ total += 1;
14125
+ if (record.ok === true) passed += 1;
14126
+ }
14127
+ }
14128
+ return total ? { passed, total } : void 0;
14129
+ }
13688
14130
  function profileHttpStatusSummaryMarkdown(result) {
13689
14131
  const httpStatusChecks = result.checks.filter((check) => check.type === "http_status");
13690
14132
  const lines = [];
@@ -13715,10 +14157,12 @@ function profileHttpStatusSummaryMarkdown(result) {
13715
14157
  profileHttpStatusAssertionKeys(evidence, viewports, "body_not_patterns"),
13716
14158
  false
13717
14159
  );
14160
+ const bodyJsonAssertions = profileHttpStatusJsonAssertionCount(viewports);
13718
14161
  const bodyParts = [
13719
14162
  bodyContains ? `body_contains ${bodyContains.passed}/${bodyContains.total}` : "",
13720
14163
  bodyNotContains ? `body_not_contains clean ${bodyNotContains.passed}/${bodyNotContains.total}` : "",
13721
- bodyNotPatterns ? `body_not_patterns clean ${bodyNotPatterns.passed}/${bodyNotPatterns.total}` : ""
14164
+ bodyNotPatterns ? `body_not_patterns clean ${bodyNotPatterns.passed}/${bodyNotPatterns.total}` : "",
14165
+ bodyJsonAssertions ? `body_json_assertions ${bodyJsonAssertions.passed}/${bodyJsonAssertions.total}` : ""
13722
14166
  ].filter(Boolean);
13723
14167
  lines.push(
13724
14168
  `- ${label}: ${method}${url ? ` ${markdownInlineCode(url)}` : ""}, statuses ${statuses.length ? statuses.join("/") : "unknown"}${bodyParts.length ? `, ${bodyParts.join(", ")}` : ""}, failures ${failedTotal}`