@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/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,187 @@ 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 compactJsonAssertionSample(value, depth = 0) {
7185
+ if (typeof value === "string") return value.length > 240 ? `${value.slice(0, 237)}...` : value;
7186
+ if (value === null || typeof value === "boolean" || typeof value === "number") return toJsonValue(value);
7187
+ if (Array.isArray(value)) {
7188
+ if (depth >= 2) return `[array:${value.length}]`;
7189
+ return value.slice(0, 3).map((item) => compactJsonAssertionSample(item, depth + 1));
7190
+ }
7191
+ if (isRecord(value)) {
7192
+ const entries = Object.entries(value).slice(0, 8);
7193
+ if (depth >= 2) return `[object:${Object.keys(value).length} keys]`;
7194
+ return Object.fromEntries(entries.map(([key, child]) => [key, compactJsonAssertionSample(child, depth + 1)]));
7195
+ }
7196
+ return String(value);
7197
+ }
7198
+ function attachJsonAssertionObservedValue(result, value) {
7199
+ const type = jsonValueType(value);
7200
+ if (type === "array" && Array.isArray(value)) {
7201
+ result.observed_length = value.length;
7202
+ result.observed_omitted_count = Math.max(0, value.length - 3);
7203
+ result.observed_sample = compactJsonAssertionSample(value);
7204
+ return;
7205
+ }
7206
+ if (type === "object" && isRecord(value)) {
7207
+ const keyCount = Object.keys(value).length;
7208
+ result.observed_key_count = keyCount;
7209
+ result.observed_omitted_count = Math.max(0, keyCount - 8);
7210
+ result.observed_sample = compactJsonAssertionSample(value);
7211
+ return;
7212
+ }
7213
+ result.observed = toJsonValue(value);
7214
+ }
7215
+ function deepJsonEqual(left, right) {
7216
+ if (left === right) return true;
7217
+ if (typeof left !== typeof right) return false;
7218
+ if (left === null || right === null) return left === right;
7219
+ if (typeof left !== "object" || typeof right !== "object") return false;
7220
+ if (Array.isArray(left) || Array.isArray(right)) {
7221
+ if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) return false;
7222
+ return left.every((item, index) => deepJsonEqual(item, right[index]));
7223
+ }
7224
+ if (!isRecord(left) || !isRecord(right)) return false;
7225
+ const leftKeys = Object.keys(left).sort();
7226
+ const rightKeys = Object.keys(right).sort();
7227
+ if (!deepJsonEqual(leftKeys, rightKeys)) return false;
7228
+ return leftKeys.every((key) => deepJsonEqual(left[key], right[key]));
7229
+ }
7230
+ function jsonContains(observed, expected) {
7231
+ if (typeof observed === "string" && typeof expected === "string") {
7232
+ return observed.includes(expected);
7233
+ }
7234
+ if (Array.isArray(observed)) {
7235
+ return observed.some((item) => deepJsonEqual(item, expected));
7236
+ }
7237
+ if (isRecord(observed) && isRecord(expected)) {
7238
+ return Object.entries(expected).every(([key, value]) => hasOwn(observed, key) && deepJsonEqual(observed[key], value));
7239
+ }
7240
+ return false;
7241
+ }
7242
+ function parseJsonPathSegments(path7) {
7243
+ let input = path7.trim();
7244
+ if (!input) throw new Error("path is empty");
7245
+ if (input === "$") return [];
7246
+ if (input.startsWith("$.")) input = input.slice(2);
7247
+ else if (input.startsWith("$[")) input = input.slice(1);
7248
+ const segments = [];
7249
+ let token = "";
7250
+ const pushToken = () => {
7251
+ if (!token) return;
7252
+ segments.push(token);
7253
+ token = "";
7254
+ };
7255
+ for (let index = 0; index < input.length; index += 1) {
7256
+ const char = input[index];
7257
+ if (char === ".") {
7258
+ pushToken();
7259
+ continue;
7260
+ }
7261
+ if (char !== "[") {
7262
+ token += char;
7263
+ continue;
7264
+ }
7265
+ pushToken();
7266
+ const closeIndex = input.indexOf("]", index + 1);
7267
+ if (closeIndex === -1) throw new Error(`unterminated bracket at ${index}`);
7268
+ const bracket = input.slice(index + 1, closeIndex).trim();
7269
+ if (!bracket) throw new Error(`empty bracket at ${index}`);
7270
+ if (/^\d+$/.test(bracket)) {
7271
+ segments.push(Number(bracket));
7272
+ } else if (bracket.startsWith('"') && bracket.endsWith('"') || bracket.startsWith("'") && bracket.endsWith("'")) {
7273
+ const quoted = bracket.startsWith("'") ? `"${bracket.slice(1, -1).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"` : bracket;
7274
+ segments.push(String(JSON.parse(quoted)));
7275
+ } else {
7276
+ segments.push(bracket);
7277
+ }
7278
+ index = closeIndex;
7279
+ }
7280
+ pushToken();
7281
+ return segments;
7282
+ }
7283
+ function resolveJsonPath(root, path7) {
7284
+ let segments;
7285
+ try {
7286
+ segments = parseJsonPathSegments(path7);
7287
+ } catch (error) {
7288
+ return { exists: false, error: String(error instanceof Error ? error.message : error) };
7289
+ }
7290
+ let current = root;
7291
+ for (const segment of segments) {
7292
+ if (typeof segment === "number") {
7293
+ if (!Array.isArray(current) || segment < 0 || segment >= current.length) return { exists: false };
7294
+ current = current[segment];
7295
+ continue;
7296
+ }
7297
+ if (!isRecord(current) || !hasOwn(current, segment)) return { exists: false };
7298
+ current = current[segment];
7299
+ }
7300
+ return { exists: true, value: current };
7301
+ }
7302
+ function evaluateHttpStatusBodyJsonAssertion(root, assertion) {
7303
+ const resolved = resolveJsonPath(root, assertion.path);
7304
+ const errors = [];
7305
+ const result = {
7306
+ label: assertion.label || assertion.path,
7307
+ path: assertion.path,
7308
+ ok: true,
7309
+ exists: resolved.exists,
7310
+ observed_type: resolved.exists ? jsonValueType(resolved.value) : "missing"
7311
+ };
7312
+ if (resolved.exists) attachJsonAssertionObservedValue(result, resolved.value);
7313
+ if (resolved.error) errors.push(resolved.error);
7314
+ if (hasOwn(assertion, "exists")) {
7315
+ result.expected_exists = assertion.exists;
7316
+ if (resolved.exists !== assertion.exists) errors.push(`expected exists=${assertion.exists}`);
7317
+ }
7318
+ if (hasOwn(assertion, "type")) {
7319
+ result.type = assertion.type;
7320
+ if (!resolved.exists || jsonValueType(resolved.value) !== assertion.type) errors.push(`expected type ${assertion.type}`);
7321
+ }
7322
+ if (hasOwn(assertion, "equals")) {
7323
+ result.equals = assertion.equals;
7324
+ if (!resolved.exists || !deepJsonEqual(resolved.value, assertion.equals)) errors.push("expected JSON value equality");
7325
+ }
7326
+ if (hasOwn(assertion, "not_equals")) {
7327
+ result.not_equals = assertion.not_equals;
7328
+ if (resolved.exists && deepJsonEqual(resolved.value, assertion.not_equals)) errors.push("expected JSON value inequality");
7329
+ }
7330
+ if (hasOwn(assertion, "contains")) {
7331
+ result.contains = assertion.contains;
7332
+ if (!resolved.exists || !jsonContains(resolved.value, assertion.contains)) errors.push("expected JSON value containment");
7333
+ }
7334
+ result.ok = errors.length === 0;
7335
+ if (errors.length) result.errors = errors;
7336
+ return result;
7337
+ }
7338
+ function evaluateHttpStatusBodyJsonAssertions(bodyText, assertions) {
7339
+ const expected = assertions?.filter((assertion) => assertion.path) ?? [];
7340
+ if (!expected.length) return [];
7341
+ let parsed;
7342
+ try {
7343
+ parsed = JSON.parse(bodyText);
7344
+ } catch (error) {
7345
+ const message = `response body is not valid JSON: ${String(error instanceof Error ? error.message : error).slice(0, 200)}`;
7346
+ return expected.map((assertion) => ({
7347
+ label: assertion.label || assertion.path,
7348
+ path: assertion.path,
7349
+ ok: false,
7350
+ exists: false,
7351
+ observed_type: "missing",
7352
+ errors: [message]
7353
+ }));
7354
+ }
7355
+ return expected.map((assertion) => evaluateHttpStatusBodyJsonAssertion(parsed, assertion));
7356
+ }
7173
7357
  function compactProfileSetupSummaryText(value, limit = 160) {
7174
7358
  const text = typeof value === "string" ? value.replace(/\s+/g, " ").trim() : "";
7175
7359
  if (!text) return void 0;
@@ -7824,6 +8008,46 @@ function validateRegexPatterns(patterns, label) {
7824
8008
  }
7825
8009
  }
7826
8010
  }
8011
+ function normalizeHttpStatusBodyJsonAssertions(value, label) {
8012
+ if (value === void 0) return void 0;
8013
+ if (!Array.isArray(value)) throw new Error(`${label} must be an array.`);
8014
+ if (!value.length) throw new Error(`${label} must not be empty.`);
8015
+ return value.map((item, index) => {
8016
+ const itemLabel = `${label}[${index}]`;
8017
+ if (typeof item === "string") {
8018
+ const path8 = stringValue2(item);
8019
+ if (!path8) throw new Error(`${itemLabel} path must not be empty.`);
8020
+ return { path: path8, exists: true };
8021
+ }
8022
+ if (!isRecord(item)) throw new Error(`${itemLabel} must be an object or JSON path string.`);
8023
+ const path7 = stringFromOwn(item, "path", "json_path", "jsonPath", "key");
8024
+ if (!path7) throw new Error(`${itemLabel}.path is required.`);
8025
+ const assertion = {
8026
+ label: stringValue2(item.label),
8027
+ path: path7
8028
+ };
8029
+ const exists = booleanValue(valueFromOwn(item, "exists", "present"));
8030
+ if (exists !== void 0) assertion.exists = exists;
8031
+ const type = stringValue2(valueFromOwn(item, "type", "value_type", "valueType"));
8032
+ if (type !== void 0) {
8033
+ const allowedTypes = ["array", "boolean", "null", "number", "object", "string"];
8034
+ if (!allowedTypes.includes(type)) {
8035
+ throw new Error(`${itemLabel}.type must be one of ${allowedTypes.join(", ")}.`);
8036
+ }
8037
+ assertion.type = type;
8038
+ }
8039
+ const equalsValue = valueFromOwn(item, "equals", "expected", "expected_value", "expectedValue", "value");
8040
+ if (equalsValue !== void 0) assertion.equals = toJsonValue(equalsValue);
8041
+ const notEqualsValue = valueFromOwn(item, "not_equals", "notEquals", "forbidden", "forbidden_value", "forbiddenValue");
8042
+ if (notEqualsValue !== void 0) assertion.not_equals = toJsonValue(notEqualsValue);
8043
+ const containsValue = valueFromOwn(item, "contains", "includes", "contains_value", "containsValue", "include");
8044
+ if (containsValue !== void 0) assertion.contains = toJsonValue(containsValue);
8045
+ if (assertion.exists === void 0 && assertion.type === void 0 && !hasOwn(assertion, "equals") && !hasOwn(assertion, "not_equals") && !hasOwn(assertion, "contains")) {
8046
+ assertion.exists = true;
8047
+ }
8048
+ return assertion;
8049
+ });
8050
+ }
7827
8051
  function isDialogCountCheckType(type) {
7828
8052
  return type === "dialog_count_equals" || type === "dialog_accept_count_equals" || type === "dialog_dismiss_count_equals";
7829
8053
  }
@@ -7923,6 +8147,10 @@ function normalizeCheck(input, index) {
7923
8147
  `checks[${index}] body_not_patterns`
7924
8148
  ) : void 0;
7925
8149
  if (bodyNotPatterns?.length) validateRegexPatterns(bodyNotPatterns, `checks[${index}] body_not_patterns`);
8150
+ const bodyJsonAssertions = isHttpStatusCheck ? normalizeHttpStatusBodyJsonAssertions(
8151
+ input.body_json_assertions ?? input.bodyJsonAssertions ?? input.json_body_assertions ?? input.jsonBodyAssertions ?? input.json_assertions ?? input.jsonAssertions ?? input.response_json_assertions ?? input.responseJsonAssertions,
8152
+ `checks[${index}] body_json_assertions`
8153
+ ) : void 0;
7926
8154
  if (isLinkStatusCheck) {
7927
8155
  if (minCount !== void 0 && (!Number.isInteger(minCount) || minCount < 0)) {
7928
8156
  throw new Error(`checks[${index}] ${type} min_count must be a non-negative integer.`);
@@ -7948,6 +8176,7 @@ function normalizeCheck(input, index) {
7948
8176
  body_contains: bodyContains,
7949
8177
  body_not_contains: bodyNotContains,
7950
8178
  body_not_patterns: bodyNotPatterns,
8179
+ body_json_assertions: bodyJsonAssertions,
7951
8180
  expected_texts: expectedTexts,
7952
8181
  link_selector: stringValue2(input.link_selector) || stringValue2(input.linkSelector),
7953
8182
  source_selector: stringValue2(input.source_selector) || stringValue2(input.sourceSelector),
@@ -8154,6 +8383,38 @@ function httpStatusBodyNotPatternFailures(result, check) {
8154
8383
  const observed = isRecord(result.body_not_patterns) ? result.body_not_patterns : {};
8155
8384
  return forbidden.filter((pattern) => observed[pattern] !== false);
8156
8385
  }
8386
+ function httpStatusBodyJsonAssertionFailures(result, check) {
8387
+ const expected = check.body_json_assertions?.filter((assertion) => assertion.path) ?? [];
8388
+ if (!expected.length) return [];
8389
+ if (!Array.isArray(result.body_json_assertions)) {
8390
+ return expected.map((assertion) => ({
8391
+ label: assertion.label || assertion.path,
8392
+ path: assertion.path,
8393
+ ok: false,
8394
+ exists: false,
8395
+ observed_type: "missing",
8396
+ errors: ["body_json_assertions evidence missing"]
8397
+ }));
8398
+ }
8399
+ return result.body_json_assertions.filter((assertion) => isRecord(assertion) && assertion.ok !== true).map((assertion) => ({
8400
+ label: stringValue2(assertion.label) || stringValue2(assertion.path) || "json assertion",
8401
+ path: stringValue2(assertion.path) || "",
8402
+ ok: false,
8403
+ exists: assertion.exists === true,
8404
+ observed: hasOwn(assertion, "observed") ? toJsonValue(assertion.observed) : void 0,
8405
+ observed_sample: hasOwn(assertion, "observed_sample") ? toJsonValue(assertion.observed_sample) : void 0,
8406
+ observed_length: numberValue(assertion.observed_length),
8407
+ observed_key_count: numberValue(assertion.observed_key_count),
8408
+ observed_omitted_count: numberValue(assertion.observed_omitted_count),
8409
+ observed_type: stringValue2(assertion.observed_type) || "missing",
8410
+ expected_exists: booleanValue(assertion.expected_exists),
8411
+ equals: hasOwn(assertion, "equals") ? toJsonValue(assertion.equals) : void 0,
8412
+ not_equals: hasOwn(assertion, "not_equals") ? toJsonValue(assertion.not_equals) : void 0,
8413
+ contains: hasOwn(assertion, "contains") ? toJsonValue(assertion.contains) : void 0,
8414
+ type: stringValue2(assertion.type),
8415
+ errors: Array.isArray(assertion.errors) ? assertion.errors.map(String) : void 0
8416
+ }));
8417
+ }
8157
8418
  function linkStatusResultOk(result, check) {
8158
8419
  const status = numberValue(result.status);
8159
8420
  if (!httpStatusIsAllowed(status, check)) return false;
@@ -8171,6 +8432,7 @@ function linkStatusResultOk(result, check) {
8171
8432
  if (httpStatusBodyContainsFailures(result, check).length) return false;
8172
8433
  if (httpStatusBodyNotContainsFailures(result, check).length) return false;
8173
8434
  if (httpStatusBodyNotPatternFailures(result, check).length) return false;
8435
+ if (httpStatusBodyJsonAssertionFailures(result, check).length) return false;
8174
8436
  return true;
8175
8437
  }
8176
8438
  function responseHeader(response, name) {
@@ -8239,7 +8501,7 @@ async function preflightHttpStatusCheck(check, index, targetUrl, fetchImpl) {
8239
8501
  statusText = typeof response.statusText === "string" ? response.statusText : "";
8240
8502
  result.content_type = responseHeader(response, "content-type");
8241
8503
  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);
8504
+ 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
8505
  if (shouldReadBody && method !== "HEAD") {
8244
8506
  const body = await responseBodyText(response);
8245
8507
  result.bytes = body.bytes;
@@ -8252,6 +8514,9 @@ async function preflightHttpStatusCheck(check, index, targetUrl, fetchImpl) {
8252
8514
  if (check.body_not_patterns?.length) {
8253
8515
  result.body_not_patterns = Object.fromEntries(check.body_not_patterns.filter(Boolean).map((pattern) => [pattern, new RegExp(pattern).test(body.text)]));
8254
8516
  }
8517
+ if (check.body_json_assertions?.length) {
8518
+ result.body_json_assertions = evaluateHttpStatusBodyJsonAssertions(body.text, check.body_json_assertions);
8519
+ }
8255
8520
  }
8256
8521
  } catch (caught) {
8257
8522
  error = String(caught instanceof Error ? caught.message : caught).slice(0, 500);
@@ -8260,6 +8525,7 @@ async function preflightHttpStatusCheck(check, index, targetUrl, fetchImpl) {
8260
8525
  const bodyContainsMissing = httpStatusBodyContainsFailures(result, check);
8261
8526
  const bodyNotContainsFound = httpStatusBodyNotContainsFailures(result, check);
8262
8527
  const bodyNotPatternsFound = httpStatusBodyNotPatternFailures(result, check);
8528
+ const bodyJsonAssertionsFailed = httpStatusBodyJsonAssertionFailures(result, check);
8263
8529
  const ok = !error && linkStatusResultOk(result, check);
8264
8530
  return {
8265
8531
  index,
@@ -8278,7 +8544,9 @@ async function preflightHttpStatusCheck(check, index, targetUrl, fetchImpl) {
8278
8544
  body_not_contains: isRecord(result.body_not_contains) ? Object.fromEntries(Object.entries(result.body_not_contains).map(([key, value]) => [key, value === true])) : null,
8279
8545
  body_not_contains_found: bodyNotContainsFound,
8280
8546
  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
8547
+ body_not_patterns_found: bodyNotPatternsFound,
8548
+ body_json_assertions: Array.isArray(result.body_json_assertions) ? result.body_json_assertions : null,
8549
+ body_json_assertions_failed: bodyJsonAssertionsFailed
8282
8550
  };
8283
8551
  }
8284
8552
  async function preflightRiddleProofProfileHttpStatusChecks(profile, options = {}) {
@@ -8323,6 +8591,7 @@ function summarizeHttpStatusEvidence(viewport, check) {
8323
8591
  const bodyContainsMissing = httpStatusBodyContainsFailures(statusEvidence, check);
8324
8592
  const bodyNotContainsFound = httpStatusBodyNotContainsFailures(statusEvidence, check);
8325
8593
  const bodyNotPatternsFound = httpStatusBodyNotPatternFailures(statusEvidence, check);
8594
+ const bodyJsonAssertionsFailed = httpStatusBodyJsonAssertionFailures(statusEvidence, check);
8326
8595
  if (!linkStatusResultOk(statusEvidence, check)) {
8327
8596
  failures.push({
8328
8597
  code: "http_status_failed",
@@ -8341,6 +8610,8 @@ function summarizeHttpStatusEvidence(viewport, check) {
8341
8610
  body_not_contains_found: bodyNotContainsFound,
8342
8611
  body_not_patterns: check.body_not_patterns ?? null,
8343
8612
  body_not_patterns_found: bodyNotPatternsFound,
8613
+ body_json_assertions: check.body_json_assertions ?? null,
8614
+ body_json_assertions_failed: bodyJsonAssertionsFailed.map((assertion) => toJsonValue(assertion)),
8344
8615
  body_sample: stringValue2(statusEvidence.body_sample) ?? null
8345
8616
  });
8346
8617
  }
@@ -8362,6 +8633,8 @@ function summarizeHttpStatusEvidence(viewport, check) {
8362
8633
  body_not_contains_found: bodyNotContainsFound,
8363
8634
  body_not_patterns: isRecord(statusEvidence.body_not_patterns) ? toJsonValue(statusEvidence.body_not_patterns) : null,
8364
8635
  body_not_patterns_found: bodyNotPatternsFound,
8636
+ body_json_assertions: Array.isArray(statusEvidence.body_json_assertions) ? toJsonValue(statusEvidence.body_json_assertions) : null,
8637
+ body_json_assertions_failed: bodyJsonAssertionsFailed.map((assertion) => toJsonValue(assertion)),
8365
8638
  body_sample: stringValue2(statusEvidence.body_sample) ?? null,
8366
8639
  failures
8367
8640
  };
@@ -8998,6 +9271,7 @@ function assessCheckFromEvidence(check, evidence) {
8998
9271
  body_contains: check.body_contains ?? [],
8999
9272
  body_not_contains: check.body_not_contains ?? [],
9000
9273
  body_not_patterns: check.body_not_patterns ?? [],
9274
+ body_json_assertions: toJsonValue(check.body_json_assertions ?? []),
9001
9275
  viewports: summaries.map((summary) => toJsonValue(summary)),
9002
9276
  failures: failed.flatMap((summary) => Array.isArray(summary.failures) ? summary.failures.map((failure) => toJsonValue({ viewport: stringValue2(summary.viewport) ?? null, failure })) : [])
9003
9277
  },
@@ -9725,6 +9999,40 @@ function httpStatusBodyNotPatternFailures(result, check) {
9725
9999
  : {};
9726
10000
  return forbidden.filter((pattern) => observed[pattern] !== false);
9727
10001
  }
10002
+ function httpStatusBodyJsonAssertionFailures(result, check) {
10003
+ const expected = Array.isArray(check.body_json_assertions) ? check.body_json_assertions.filter((assertion) => assertion && assertion.path) : [];
10004
+ if (!expected.length) return [];
10005
+ if (!Array.isArray(result.body_json_assertions)) {
10006
+ return expected.map((assertion) => ({
10007
+ label: assertion.label || assertion.path,
10008
+ path: assertion.path,
10009
+ ok: false,
10010
+ exists: false,
10011
+ observed_type: "missing",
10012
+ errors: ["body_json_assertions evidence missing"],
10013
+ }));
10014
+ }
10015
+ return result.body_json_assertions
10016
+ .filter((assertion) => assertion && typeof assertion === "object" && assertion.ok !== true)
10017
+ .map((assertion) => ({
10018
+ label: typeof assertion.label === "string" && assertion.label ? assertion.label : typeof assertion.path === "string" && assertion.path ? assertion.path : "json assertion",
10019
+ path: typeof assertion.path === "string" ? assertion.path : "",
10020
+ ok: false,
10021
+ exists: assertion.exists === true,
10022
+ observed: Object.hasOwn(assertion, "observed") ? assertion.observed : undefined,
10023
+ observed_sample: Object.hasOwn(assertion, "observed_sample") ? assertion.observed_sample : undefined,
10024
+ observed_length: typeof assertion.observed_length === "number" && Number.isFinite(assertion.observed_length) ? assertion.observed_length : undefined,
10025
+ observed_key_count: typeof assertion.observed_key_count === "number" && Number.isFinite(assertion.observed_key_count) ? assertion.observed_key_count : undefined,
10026
+ observed_omitted_count: typeof assertion.observed_omitted_count === "number" && Number.isFinite(assertion.observed_omitted_count) ? assertion.observed_omitted_count : undefined,
10027
+ observed_type: typeof assertion.observed_type === "string" && assertion.observed_type ? assertion.observed_type : "missing",
10028
+ expected_exists: typeof assertion.expected_exists === "boolean" ? assertion.expected_exists : undefined,
10029
+ equals: Object.hasOwn(assertion, "equals") ? assertion.equals : undefined,
10030
+ not_equals: Object.hasOwn(assertion, "not_equals") ? assertion.not_equals : undefined,
10031
+ contains: Object.hasOwn(assertion, "contains") ? assertion.contains : undefined,
10032
+ type: typeof assertion.type === "string" ? assertion.type : undefined,
10033
+ errors: Array.isArray(assertion.errors) ? assertion.errors.map(String) : undefined,
10034
+ }));
10035
+ }
9728
10036
  function linkStatusResultOk(result, check) {
9729
10037
  if (!result || typeof result !== "object" || Array.isArray(result)) return false;
9730
10038
  if (!httpStatusIsAllowed(result.status, check)) return false;
@@ -9742,6 +10050,7 @@ function linkStatusResultOk(result, check) {
9742
10050
  if (httpStatusBodyContainsFailures(result, check).length) return false;
9743
10051
  if (httpStatusBodyNotContainsFailures(result, check).length) return false;
9744
10052
  if (httpStatusBodyNotPatternFailures(result, check).length) return false;
10053
+ if (httpStatusBodyJsonAssertionFailures(result, check).length) return false;
9745
10054
  return true;
9746
10055
  }
9747
10056
  function summarizeHttpStatusEvidence(viewport, check) {
@@ -9762,6 +10071,7 @@ function summarizeHttpStatusEvidence(viewport, check) {
9762
10071
  const bodyContainsMissing = httpStatusBodyContainsFailures(statusEvidence, check);
9763
10072
  const bodyNotContainsFound = httpStatusBodyNotContainsFailures(statusEvidence, check);
9764
10073
  const bodyNotPatternsFound = httpStatusBodyNotPatternFailures(statusEvidence, check);
10074
+ const bodyJsonAssertionsFailed = httpStatusBodyJsonAssertionFailures(statusEvidence, check);
9765
10075
  if (!linkStatusResultOk(statusEvidence, check)) {
9766
10076
  failures.push({
9767
10077
  code: "http_status_failed",
@@ -9780,6 +10090,8 @@ function summarizeHttpStatusEvidence(viewport, check) {
9780
10090
  body_not_contains_found: bodyNotContainsFound,
9781
10091
  body_not_patterns: Array.isArray(check.body_not_patterns) ? check.body_not_patterns : null,
9782
10092
  body_not_patterns_found: bodyNotPatternsFound,
10093
+ body_json_assertions: Array.isArray(check.body_json_assertions) ? check.body_json_assertions : null,
10094
+ body_json_assertions_failed: bodyJsonAssertionsFailed,
9783
10095
  body_sample: typeof statusEvidence.body_sample === "string" ? statusEvidence.body_sample : null,
9784
10096
  });
9785
10097
  }
@@ -9807,6 +10119,8 @@ function summarizeHttpStatusEvidence(viewport, check) {
9807
10119
  ? statusEvidence.body_not_patterns
9808
10120
  : null,
9809
10121
  body_not_patterns_found: bodyNotPatternsFound,
10122
+ body_json_assertions: Array.isArray(statusEvidence.body_json_assertions) ? statusEvidence.body_json_assertions : null,
10123
+ body_json_assertions_failed: bodyJsonAssertionsFailed,
9810
10124
  body_sample: typeof statusEvidence.body_sample === "string" ? statusEvidence.body_sample : null,
9811
10125
  failures,
9812
10126
  };
@@ -11909,6 +12223,187 @@ function linkProbeResponseFields(response, method) {
11909
12223
  content_length: contentLength,
11910
12224
  };
11911
12225
  }
12226
+ function jsonProbeValueType(value) {
12227
+ if (value === null) return "null";
12228
+ if (Array.isArray(value)) return "array";
12229
+ if (typeof value === "boolean") return "boolean";
12230
+ if (typeof value === "number") return "number";
12231
+ if (typeof value === "string") return "string";
12232
+ return "object";
12233
+ }
12234
+ function compactJsonProbeSample(value, depth) {
12235
+ const level = typeof depth === "number" ? depth : 0;
12236
+ if (typeof value === "string") return value.length > 240 ? value.slice(0, 237) + "..." : value;
12237
+ if (value === null || typeof value === "boolean" || typeof value === "number") return value;
12238
+ if (Array.isArray(value)) {
12239
+ if (level >= 2) return "[array:" + value.length + "]";
12240
+ return value.slice(0, 3).map((item) => compactJsonProbeSample(item, level + 1));
12241
+ }
12242
+ if (value && typeof value === "object") {
12243
+ const entries = Object.entries(value);
12244
+ if (level >= 2) return "[object:" + entries.length + " keys]";
12245
+ return Object.fromEntries(entries.slice(0, 8).map(([key, child]) => [key, compactJsonProbeSample(child, level + 1)]));
12246
+ }
12247
+ return String(value);
12248
+ }
12249
+ function attachJsonProbeObservedValue(result, value) {
12250
+ const type = jsonProbeValueType(value);
12251
+ if (type === "array" && Array.isArray(value)) {
12252
+ result.observed_length = value.length;
12253
+ result.observed_omitted_count = Math.max(0, value.length - 3);
12254
+ result.observed_sample = compactJsonProbeSample(value, 0);
12255
+ return;
12256
+ }
12257
+ if (type === "object" && value && typeof value === "object" && !Array.isArray(value)) {
12258
+ const keyCount = Object.keys(value).length;
12259
+ result.observed_key_count = keyCount;
12260
+ result.observed_omitted_count = Math.max(0, keyCount - 8);
12261
+ result.observed_sample = compactJsonProbeSample(value, 0);
12262
+ return;
12263
+ }
12264
+ result.observed = value;
12265
+ }
12266
+ function jsonProbeDeepEqual(left, right) {
12267
+ if (left === right) return true;
12268
+ if (typeof left !== typeof right) return false;
12269
+ if (left === null || right === null) return left === right;
12270
+ if (typeof left !== "object" || typeof right !== "object") return false;
12271
+ if (Array.isArray(left) || Array.isArray(right)) {
12272
+ if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) return false;
12273
+ return left.every((item, index) => jsonProbeDeepEqual(item, right[index]));
12274
+ }
12275
+ const leftKeys = Object.keys(left).sort();
12276
+ const rightKeys = Object.keys(right).sort();
12277
+ if (!jsonProbeDeepEqual(leftKeys, rightKeys)) return false;
12278
+ return leftKeys.every((key) => jsonProbeDeepEqual(left[key], right[key]));
12279
+ }
12280
+ function jsonProbeContains(observed, expected) {
12281
+ if (typeof observed === "string" && typeof expected === "string") return observed.includes(expected);
12282
+ if (Array.isArray(observed)) return observed.some((item) => jsonProbeDeepEqual(item, expected));
12283
+ if (observed && expected && typeof observed === "object" && typeof expected === "object" && !Array.isArray(observed) && !Array.isArray(expected)) {
12284
+ return Object.entries(expected).every(([key, value]) => Object.hasOwn(observed, key) && jsonProbeDeepEqual(observed[key], value));
12285
+ }
12286
+ return false;
12287
+ }
12288
+ function parseJsonProbePathSegments(path) {
12289
+ let input = String(path || "").trim();
12290
+ if (!input) throw new Error("path is empty");
12291
+ if (input === "$") return [];
12292
+ if (input.startsWith("$.")) input = input.slice(2);
12293
+ else if (input.startsWith("$[")) input = input.slice(1);
12294
+ const segments = [];
12295
+ let token = "";
12296
+ const pushToken = () => {
12297
+ if (!token) return;
12298
+ segments.push(token);
12299
+ token = "";
12300
+ };
12301
+ for (let index = 0; index < input.length; index += 1) {
12302
+ const char = input[index];
12303
+ if (char === ".") {
12304
+ pushToken();
12305
+ continue;
12306
+ }
12307
+ if (char !== "[") {
12308
+ token += char;
12309
+ continue;
12310
+ }
12311
+ pushToken();
12312
+ const closeIndex = input.indexOf("]", index + 1);
12313
+ if (closeIndex === -1) throw new Error("unterminated bracket at " + index);
12314
+ const bracket = input.slice(index + 1, closeIndex).trim();
12315
+ if (!bracket) throw new Error("empty bracket at " + index);
12316
+ if (/^\d+$/.test(bracket)) {
12317
+ segments.push(Number(bracket));
12318
+ } else if ((bracket.startsWith('"') && bracket.endsWith('"')) || (bracket.startsWith("'") && bracket.endsWith("'"))) {
12319
+ const quoted = bracket.startsWith("'")
12320
+ ? '"' + bracket.slice(1, -1).replace(/\\/g, "\\\\").replace(/"/g, "\\\"") + '"'
12321
+ : bracket;
12322
+ segments.push(String(JSON.parse(quoted)));
12323
+ } else {
12324
+ segments.push(bracket);
12325
+ }
12326
+ index = closeIndex;
12327
+ }
12328
+ pushToken();
12329
+ return segments;
12330
+ }
12331
+ function resolveJsonProbePath(root, path) {
12332
+ let segments;
12333
+ try {
12334
+ segments = parseJsonProbePathSegments(path);
12335
+ } catch (error) {
12336
+ return { exists: false, error: String(error && error.message ? error.message : error) };
12337
+ }
12338
+ let current = root;
12339
+ for (const segment of segments) {
12340
+ if (typeof segment === "number") {
12341
+ if (!Array.isArray(current) || segment < 0 || segment >= current.length) return { exists: false };
12342
+ current = current[segment];
12343
+ continue;
12344
+ }
12345
+ if (!current || typeof current !== "object" || Array.isArray(current) || !Object.hasOwn(current, segment)) {
12346
+ return { exists: false };
12347
+ }
12348
+ current = current[segment];
12349
+ }
12350
+ return { exists: true, value: current };
12351
+ }
12352
+ function evaluateJsonProbeAssertion(root, assertion) {
12353
+ const resolved = resolveJsonProbePath(root, assertion.path);
12354
+ const errors = [];
12355
+ const result = {
12356
+ label: assertion.label || assertion.path,
12357
+ path: assertion.path,
12358
+ ok: true,
12359
+ exists: resolved.exists,
12360
+ observed_type: resolved.exists ? jsonProbeValueType(resolved.value) : "missing",
12361
+ };
12362
+ if (resolved.exists) attachJsonProbeObservedValue(result, resolved.value);
12363
+ if (resolved.error) errors.push(resolved.error);
12364
+ if (Object.hasOwn(assertion, "exists")) {
12365
+ result.expected_exists = assertion.exists;
12366
+ if (resolved.exists !== assertion.exists) errors.push("expected exists=" + assertion.exists);
12367
+ }
12368
+ if (Object.hasOwn(assertion, "type")) {
12369
+ result.type = assertion.type;
12370
+ if (!resolved.exists || jsonProbeValueType(resolved.value) !== assertion.type) errors.push("expected type " + assertion.type);
12371
+ }
12372
+ if (Object.hasOwn(assertion, "equals")) {
12373
+ result.equals = assertion.equals;
12374
+ if (!resolved.exists || !jsonProbeDeepEqual(resolved.value, assertion.equals)) errors.push("expected JSON value equality");
12375
+ }
12376
+ if (Object.hasOwn(assertion, "not_equals")) {
12377
+ result.not_equals = assertion.not_equals;
12378
+ if (resolved.exists && jsonProbeDeepEqual(resolved.value, assertion.not_equals)) errors.push("expected JSON value inequality");
12379
+ }
12380
+ if (Object.hasOwn(assertion, "contains")) {
12381
+ result.contains = assertion.contains;
12382
+ if (!resolved.exists || !jsonProbeContains(resolved.value, assertion.contains)) errors.push("expected JSON value containment");
12383
+ }
12384
+ result.ok = errors.length === 0;
12385
+ if (errors.length) result.errors = errors;
12386
+ return result;
12387
+ }
12388
+ function evaluateJsonProbeAssertions(text, assertions) {
12389
+ const expected = Array.isArray(assertions) ? assertions.filter((assertion) => assertion && assertion.path) : [];
12390
+ if (!expected.length) return [];
12391
+ let parsed;
12392
+ try {
12393
+ parsed = JSON.parse(text);
12394
+ } catch (error) {
12395
+ const message = "response body is not valid JSON: " + String(error && error.message ? error.message : error).slice(0, 200);
12396
+ return expected.map((assertion) => ({
12397
+ label: assertion.label || assertion.path,
12398
+ path: assertion.path,
12399
+ ok: false,
12400
+ exists: false,
12401
+ observed_type: "missing",
12402
+ errors: [message],
12403
+ }));
12404
+ }
12405
+ return expected.map((assertion) => evaluateJsonProbeAssertion(parsed, assertion));
12406
+ }
11912
12407
  async function collectHttpStatus(check) {
11913
12408
  const url = httpStatusRequestUrl(check, page.url() || targetUrl);
11914
12409
  const method = httpStatusMethod(check);
@@ -11925,6 +12420,7 @@ async function collectHttpStatus(check) {
11925
12420
  const bodyContains = Array.isArray(check.body_contains) ? check.body_contains.filter(Boolean) : [];
11926
12421
  const bodyNotContains = Array.isArray(check.body_not_contains) ? check.body_not_contains.filter(Boolean) : [];
11927
12422
  const bodyNotPatterns = Array.isArray(check.body_not_patterns) ? check.body_not_patterns.filter(Boolean) : [];
12423
+ const bodyJsonAssertions = Array.isArray(check.body_json_assertions) ? check.body_json_assertions.filter((assertion) => assertion && assertion.path) : [];
11928
12424
  const options = {
11929
12425
  method,
11930
12426
  redirect: "follow",
@@ -11950,17 +12446,18 @@ async function collectHttpStatus(check) {
11950
12446
  Object.assign(result, linkProbeResponseFields(response, method));
11951
12447
  result.url = url;
11952
12448
  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;
12449
+ 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
12450
  if (shouldReadBody) {
11955
12451
  try {
11956
12452
  const buffer = await response.arrayBuffer();
11957
12453
  result.bytes = buffer.byteLength;
11958
- if (bodyContains.length || bodyNotContains.length || bodyNotPatterns.length) {
12454
+ if (bodyContains.length || bodyNotContains.length || bodyNotPatterns.length || bodyJsonAssertions.length) {
11959
12455
  const text = new TextDecoder().decode(buffer);
11960
12456
  result.body_sample = text.slice(0, 1000);
11961
12457
  if (bodyContains.length) result.body_contains = Object.fromEntries(bodyContains.map((expected) => [expected, text.includes(expected)]));
11962
12458
  if (bodyNotContains.length) result.body_not_contains = Object.fromEntries(bodyNotContains.map((forbidden) => [forbidden, text.includes(forbidden)]));
11963
12459
  if (bodyNotPatterns.length) result.body_not_patterns = Object.fromEntries(bodyNotPatterns.map((pattern) => [pattern, new RegExp(pattern).test(text)]));
12460
+ if (bodyJsonAssertions.length) result.body_json_assertions = evaluateJsonProbeAssertions(text, bodyJsonAssertions);
11964
12461
  }
11965
12462
  } catch (error) {
11966
12463
  result.error = String(error && error.message ? error.message : error).slice(0, 500);
@@ -11973,6 +12470,7 @@ async function collectHttpStatus(check) {
11973
12470
  && (!bodyContains.length || bodyContains.every((expected) => result.body_contains && result.body_contains[expected] === true))
11974
12471
  && (!bodyNotContains.length || bodyNotContains.every((forbidden) => result.body_not_contains && result.body_not_contains[forbidden] === false))
11975
12472
  && (!bodyNotPatterns.length || bodyNotPatterns.every((pattern) => result.body_not_patterns && result.body_not_patterns[pattern] === false))
12473
+ && (!bodyJsonAssertions.length || (Array.isArray(result.body_json_assertions) && result.body_json_assertions.every((assertion) => assertion.ok === true)))
11976
12474
  && !result.error;
11977
12475
  return result;
11978
12476
  } catch (error) {
@@ -13685,6 +14183,21 @@ function profileHttpStatusAssertionKeys(evidence, viewports, field) {
13685
14183
  }
13686
14184
  return [...keys];
13687
14185
  }
14186
+ function profileHttpStatusJsonAssertionCount(viewports) {
14187
+ if (!viewports.length) return void 0;
14188
+ let passed = 0;
14189
+ let total = 0;
14190
+ for (const viewport of viewports) {
14191
+ if (!Array.isArray(viewport.body_json_assertions)) continue;
14192
+ for (const assertion of viewport.body_json_assertions) {
14193
+ const record = cliRecord(assertion);
14194
+ if (!record) continue;
14195
+ total += 1;
14196
+ if (record.ok === true) passed += 1;
14197
+ }
14198
+ }
14199
+ return total ? { passed, total } : void 0;
14200
+ }
13688
14201
  function profileHttpStatusSummaryMarkdown(result) {
13689
14202
  const httpStatusChecks = result.checks.filter((check) => check.type === "http_status");
13690
14203
  const lines = [];
@@ -13715,10 +14228,12 @@ function profileHttpStatusSummaryMarkdown(result) {
13715
14228
  profileHttpStatusAssertionKeys(evidence, viewports, "body_not_patterns"),
13716
14229
  false
13717
14230
  );
14231
+ const bodyJsonAssertions = profileHttpStatusJsonAssertionCount(viewports);
13718
14232
  const bodyParts = [
13719
14233
  bodyContains ? `body_contains ${bodyContains.passed}/${bodyContains.total}` : "",
13720
14234
  bodyNotContains ? `body_not_contains clean ${bodyNotContains.passed}/${bodyNotContains.total}` : "",
13721
- bodyNotPatterns ? `body_not_patterns clean ${bodyNotPatterns.passed}/${bodyNotPatterns.total}` : ""
14235
+ bodyNotPatterns ? `body_not_patterns clean ${bodyNotPatterns.passed}/${bodyNotPatterns.total}` : "",
14236
+ bodyJsonAssertions ? `body_json_assertions ${bodyJsonAssertions.passed}/${bodyJsonAssertions.total}` : ""
13722
14237
  ].filter(Boolean);
13723
14238
  lines.push(
13724
14239
  `- ${label}: ${method}${url ? ` ${markdownInlineCode(url)}` : ""}, statuses ${statuses.length ? statuses.join("/") : "unknown"}${bodyParts.length ? `, ${bodyParts.join(", ")}` : ""}, failures ${failedTotal}`