@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/index.d.ts CHANGED
@@ -10,5 +10,5 @@ export { CreateCaptureDiagnosticInput, DEFAULT_DIAGNOSTIC_ARRAY_LIMIT, DEFAULT_D
10
10
  export { BuildVisualProofSessionInput, RIDDLE_PROOF_VISUAL_SESSION_FINGERPRINT_VERSION, RIDDLE_PROOF_VISUAL_SESSION_VERSION, VisualProofSessionMismatch, buildVisualProofSession, compareVisualProofSessionFingerprint, parseVisualProofSession, visualSessionFingerprint, visualSessionFingerprintBasis } from './proof-session.js';
11
11
  export { AssessPlayabilityOptions, RIDDLE_PROOF_PLAYABILITY_ASSESSMENT_VERSION, RIDDLE_PROOF_PLAYABILITY_VERSION, RiddleProofPlayabilityAssessment, RiddleProofPlayabilityEvidence, assessPlayabilityEvidence, extractPlayabilityEvidence, isRiddleProofPlayabilityMode } from './playability.js';
12
12
  export { AssessBasicGameplayOptions, AttachBasicGameplayArtifactOptions, BASIC_GAMEPLAY_ACTION_TYPES, BASIC_GAMEPLAY_PROGRESS_CHECK_TYPES, BasicGameplayActionResult, BasicGameplayActionType, BasicGameplayArtifactResolution, BasicGameplayAssessmentSummary, BasicGameplayBoundsOffender, BasicGameplayCanvasState, BasicGameplayCatchRecord, BasicGameplayChangeSummary, BasicGameplayFailureCode, BasicGameplayFixReference, BasicGameplayMetric, BasicGameplayMobileEvidence, BasicGameplayProgressCheckType, BasicGameplayProgressionCheck, BasicGameplayProofArtifact, BasicGameplayResponsiveViewportEvidence, BasicGameplayRouteReference, BasicGameplaySnapshot, BasicGameplaySuiteFailure, BasicGameplayWarningCode, CreateBasicGameplayCatchSummaryInput, RIDDLE_PROOF_BASIC_GAMEPLAY_ASSESSMENT_VERSION, RIDDLE_PROOF_BASIC_GAMEPLAY_CATCH_VERSION, RIDDLE_PROOF_BASIC_GAMEPLAY_VERSION, RiddleProofBasicGameplayAssessment, RiddleProofBasicGameplayCatchSummary, RiddleProofBasicGameplayEvidence, RiddleProofBasicGameplayRouteAssessment, RiddleProofBasicGameplayRouteEvidence, assessBasicGameplayEvidence, assessBasicGameplayProgressionCheck, assessBasicGameplayProgressionChecks, assessBasicGameplayRoute, attachBasicGameplayArtifactScreenshotHashes, augmentBasicGameplayAssessmentWithProgressionChecks, compactBasicGameplayText, createBasicGameplayCatchRecords, createBasicGameplayCatchSummary, extractBasicGameplayEvidence, resolveBasicGameplayProgressionCheckWithArtifactScreenshots, sanitizeBasicGameplayJsonString } from './basic-gameplay.js';
13
- export { NormalizeRiddleProofProfileOptions, RIDDLE_PROOF_PROFILE_CHECK_TYPES, RIDDLE_PROOF_PROFILE_EVIDENCE_VERSION, RIDDLE_PROOF_PROFILE_NETWORK_ABORT_ERROR_CODES, RIDDLE_PROOF_PROFILE_RESULT_VERSION, RIDDLE_PROOF_PROFILE_SETUP_ACTION_TYPES, RIDDLE_PROOF_PROFILE_STATUSES, RIDDLE_PROOF_PROFILE_VERSION, RiddleProofArtifactBodyAssertionInput, RiddleProofArtifactBodyAssertionResult, RiddleProofProfile, RiddleProofProfileArtifactRef, RiddleProofProfileBaselinePolicy, RiddleProofProfileBoundsOffender, RiddleProofProfileCheck, RiddleProofProfileCheckResult, RiddleProofProfileCheckType, RiddleProofProfileEvidence, RiddleProofProfileFailureAction, RiddleProofProfileHttpStatusPreflightCheckResult, RiddleProofProfileHttpStatusPreflightFetch, RiddleProofProfileHttpStatusPreflightFetchResponse, RiddleProofProfileHttpStatusPreflightOptions, RiddleProofProfileHttpStatusPreflightResult, RiddleProofProfileNetworkAbortErrorCode, RiddleProofProfileNetworkMock, RiddleProofProfileNetworkMockResponse, RiddleProofProfileResult, RiddleProofProfileRouteEvidence, RiddleProofProfileRouteInventoryRoute, RiddleProofProfileRunner, RiddleProofProfileSetupAction, RiddleProofProfileSetupActionType, RiddleProofProfileStatus, RiddleProofProfileTarget, RiddleProofProfileViewport, RiddleProofProfileViewportEvidence, assessRiddleProofProfileEvidence, buildRiddleProofProfileScript, collectRiddleProfileArtifactRefs, collectRiddleProofProfileWarnings, createRiddleProofProfileConfigurationError, createRiddleProofProfileEnvironmentBlockedResult, createRiddleProofProfileInsufficientResult, deriveRiddleProofArtifactBodyAssertions, extractRiddleProofProfileResult, normalizeRiddleProofProfile, preflightRiddleProofProfileHttpStatusChecks, profileStatusExitCode, resolveRiddleProofProfileRouteUrl, resolveRiddleProofProfileTargetUrl, resolveRiddleProofProfileTimeoutSec, slugifyRiddleProofProfileName, summarizeRiddleProofProfileResult } from './profile.js';
13
+ export { NormalizeRiddleProofProfileOptions, RIDDLE_PROOF_PROFILE_CHECK_TYPES, RIDDLE_PROOF_PROFILE_EVIDENCE_VERSION, RIDDLE_PROOF_PROFILE_NETWORK_ABORT_ERROR_CODES, RIDDLE_PROOF_PROFILE_RESULT_VERSION, RIDDLE_PROOF_PROFILE_SETUP_ACTION_TYPES, RIDDLE_PROOF_PROFILE_STATUSES, RIDDLE_PROOF_PROFILE_VERSION, RiddleProofArtifactBodyAssertionInput, RiddleProofArtifactBodyAssertionResult, RiddleProofProfile, RiddleProofProfileArtifactRef, RiddleProofProfileBaselinePolicy, RiddleProofProfileBoundsOffender, RiddleProofProfileCheck, RiddleProofProfileCheckResult, RiddleProofProfileCheckType, RiddleProofProfileEvidence, RiddleProofProfileFailureAction, RiddleProofProfileHttpStatusBodyJsonAssertion, RiddleProofProfileHttpStatusBodyJsonAssertionResult, RiddleProofProfileHttpStatusPreflightCheckResult, RiddleProofProfileHttpStatusPreflightFetch, RiddleProofProfileHttpStatusPreflightFetchResponse, RiddleProofProfileHttpStatusPreflightOptions, RiddleProofProfileHttpStatusPreflightResult, RiddleProofProfileJsonValueType, RiddleProofProfileNetworkAbortErrorCode, RiddleProofProfileNetworkMock, RiddleProofProfileNetworkMockResponse, RiddleProofProfileResult, RiddleProofProfileRouteEvidence, RiddleProofProfileRouteInventoryRoute, RiddleProofProfileRunner, RiddleProofProfileSetupAction, RiddleProofProfileSetupActionType, RiddleProofProfileStatus, RiddleProofProfileTarget, RiddleProofProfileViewport, RiddleProofProfileViewportEvidence, assessRiddleProofProfileEvidence, buildRiddleProofProfileScript, collectRiddleProfileArtifactRefs, collectRiddleProofProfileWarnings, createRiddleProofProfileConfigurationError, createRiddleProofProfileEnvironmentBlockedResult, createRiddleProofProfileInsufficientResult, deriveRiddleProofArtifactBodyAssertions, extractRiddleProofProfileResult, normalizeRiddleProofProfile, preflightRiddleProofProfileHttpStatusChecks, profileStatusExitCode, resolveRiddleProofProfileRouteUrl, resolveRiddleProofProfileTargetUrl, resolveRiddleProofProfileTimeoutSec, slugifyRiddleProofProfileName, summarizeRiddleProofProfileResult } from './profile.js';
14
14
  export { DEFAULT_RIDDLE_API_BASE_URL, DEFAULT_RIDDLE_API_KEY_FILE, RiddleApiError, RiddleClientConfig, RiddleFetch, RiddlePollJobOptions, RiddlePollJobResult, RiddlePollProgressSnapshot, RiddlePollSummary, RiddlePreviewDeployResult, RiddlePreviewFramework, RiddleRunScriptInput, RiddleServerPreviewInput, RiddleServerPreviewResult, createRiddleApiClient, deployRiddlePreview, deployRiddleStaticPreview, isTerminalRiddleJobStatus, parseRiddleViewport, pollRiddleJob, resolveRiddleApiKey, riddleRequestJson, runRiddleScript, runRiddleServerPreview } from './riddle-client.js';
package/dist/index.js CHANGED
@@ -62,7 +62,7 @@ import {
62
62
  resolveRiddleProofProfileTimeoutSec,
63
63
  slugifyRiddleProofProfileName,
64
64
  summarizeRiddleProofProfileResult
65
- } from "./chunk-55KDZEB3.js";
65
+ } from "./chunk-JLINSUKO.js";
66
66
  import {
67
67
  DEFAULT_RIDDLE_API_BASE_URL,
68
68
  DEFAULT_RIDDLE_API_KEY_FILE,
package/dist/profile.cjs CHANGED
@@ -186,6 +186,9 @@ function valueFromOwn(input, ...keys) {
186
186
  function numberValue(value) {
187
187
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
188
188
  }
189
+ function booleanValue(value) {
190
+ return typeof value === "boolean" ? value : void 0;
191
+ }
189
192
  function horizontalBoundsOverflowPx(value) {
190
193
  if (!isRecord(value)) return 0;
191
194
  let max = maxPositiveNumber(
@@ -280,6 +283,156 @@ function toJsonValue(value) {
280
283
  if (isRecord(value)) return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, toJsonValue(child)]));
281
284
  return String(value);
282
285
  }
286
+ function jsonValueType(value) {
287
+ if (value === null) return "null";
288
+ if (Array.isArray(value)) return "array";
289
+ if (typeof value === "boolean") return "boolean";
290
+ if (typeof value === "number") return "number";
291
+ if (typeof value === "string") return "string";
292
+ return "object";
293
+ }
294
+ function deepJsonEqual(left, right) {
295
+ if (left === right) return true;
296
+ if (typeof left !== typeof right) return false;
297
+ if (left === null || right === null) return left === right;
298
+ if (typeof left !== "object" || typeof right !== "object") return false;
299
+ if (Array.isArray(left) || Array.isArray(right)) {
300
+ if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) return false;
301
+ return left.every((item, index) => deepJsonEqual(item, right[index]));
302
+ }
303
+ if (!isRecord(left) || !isRecord(right)) return false;
304
+ const leftKeys = Object.keys(left).sort();
305
+ const rightKeys = Object.keys(right).sort();
306
+ if (!deepJsonEqual(leftKeys, rightKeys)) return false;
307
+ return leftKeys.every((key) => deepJsonEqual(left[key], right[key]));
308
+ }
309
+ function jsonContains(observed, expected) {
310
+ if (typeof observed === "string" && typeof expected === "string") {
311
+ return observed.includes(expected);
312
+ }
313
+ if (Array.isArray(observed)) {
314
+ return observed.some((item) => deepJsonEqual(item, expected));
315
+ }
316
+ if (isRecord(observed) && isRecord(expected)) {
317
+ return Object.entries(expected).every(([key, value]) => hasOwn(observed, key) && deepJsonEqual(observed[key], value));
318
+ }
319
+ return false;
320
+ }
321
+ function parseJsonPathSegments(path) {
322
+ let input = path.trim();
323
+ if (!input) throw new Error("path is empty");
324
+ if (input === "$") return [];
325
+ if (input.startsWith("$.")) input = input.slice(2);
326
+ else if (input.startsWith("$[")) input = input.slice(1);
327
+ const segments = [];
328
+ let token = "";
329
+ const pushToken = () => {
330
+ if (!token) return;
331
+ segments.push(token);
332
+ token = "";
333
+ };
334
+ for (let index = 0; index < input.length; index += 1) {
335
+ const char = input[index];
336
+ if (char === ".") {
337
+ pushToken();
338
+ continue;
339
+ }
340
+ if (char !== "[") {
341
+ token += char;
342
+ continue;
343
+ }
344
+ pushToken();
345
+ const closeIndex = input.indexOf("]", index + 1);
346
+ if (closeIndex === -1) throw new Error(`unterminated bracket at ${index}`);
347
+ const bracket = input.slice(index + 1, closeIndex).trim();
348
+ if (!bracket) throw new Error(`empty bracket at ${index}`);
349
+ if (/^\d+$/.test(bracket)) {
350
+ segments.push(Number(bracket));
351
+ } else if (bracket.startsWith('"') && bracket.endsWith('"') || bracket.startsWith("'") && bracket.endsWith("'")) {
352
+ const quoted = bracket.startsWith("'") ? `"${bracket.slice(1, -1).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"` : bracket;
353
+ segments.push(String(JSON.parse(quoted)));
354
+ } else {
355
+ segments.push(bracket);
356
+ }
357
+ index = closeIndex;
358
+ }
359
+ pushToken();
360
+ return segments;
361
+ }
362
+ function resolveJsonPath(root, path) {
363
+ let segments;
364
+ try {
365
+ segments = parseJsonPathSegments(path);
366
+ } catch (error) {
367
+ return { exists: false, error: String(error instanceof Error ? error.message : error) };
368
+ }
369
+ let current = root;
370
+ for (const segment of segments) {
371
+ if (typeof segment === "number") {
372
+ if (!Array.isArray(current) || segment < 0 || segment >= current.length) return { exists: false };
373
+ current = current[segment];
374
+ continue;
375
+ }
376
+ if (!isRecord(current) || !hasOwn(current, segment)) return { exists: false };
377
+ current = current[segment];
378
+ }
379
+ return { exists: true, value: current };
380
+ }
381
+ function evaluateHttpStatusBodyJsonAssertion(root, assertion) {
382
+ const resolved = resolveJsonPath(root, assertion.path);
383
+ const errors = [];
384
+ const result = {
385
+ label: assertion.label || assertion.path,
386
+ path: assertion.path,
387
+ ok: true,
388
+ exists: resolved.exists,
389
+ observed_type: resolved.exists ? jsonValueType(resolved.value) : "missing"
390
+ };
391
+ if (resolved.exists) result.observed = toJsonValue(resolved.value);
392
+ if (resolved.error) errors.push(resolved.error);
393
+ if (hasOwn(assertion, "exists")) {
394
+ result.expected_exists = assertion.exists;
395
+ if (resolved.exists !== assertion.exists) errors.push(`expected exists=${assertion.exists}`);
396
+ }
397
+ if (hasOwn(assertion, "type")) {
398
+ result.type = assertion.type;
399
+ if (!resolved.exists || jsonValueType(resolved.value) !== assertion.type) errors.push(`expected type ${assertion.type}`);
400
+ }
401
+ if (hasOwn(assertion, "equals")) {
402
+ result.equals = assertion.equals;
403
+ if (!resolved.exists || !deepJsonEqual(resolved.value, assertion.equals)) errors.push("expected JSON value equality");
404
+ }
405
+ if (hasOwn(assertion, "not_equals")) {
406
+ result.not_equals = assertion.not_equals;
407
+ if (resolved.exists && deepJsonEqual(resolved.value, assertion.not_equals)) errors.push("expected JSON value inequality");
408
+ }
409
+ if (hasOwn(assertion, "contains")) {
410
+ result.contains = assertion.contains;
411
+ if (!resolved.exists || !jsonContains(resolved.value, assertion.contains)) errors.push("expected JSON value containment");
412
+ }
413
+ result.ok = errors.length === 0;
414
+ if (errors.length) result.errors = errors;
415
+ return result;
416
+ }
417
+ function evaluateHttpStatusBodyJsonAssertions(bodyText, assertions) {
418
+ const expected = assertions?.filter((assertion) => assertion.path) ?? [];
419
+ if (!expected.length) return [];
420
+ let parsed;
421
+ try {
422
+ parsed = JSON.parse(bodyText);
423
+ } catch (error) {
424
+ const message = `response body is not valid JSON: ${String(error instanceof Error ? error.message : error).slice(0, 200)}`;
425
+ return expected.map((assertion) => ({
426
+ label: assertion.label || assertion.path,
427
+ path: assertion.path,
428
+ ok: false,
429
+ exists: false,
430
+ observed_type: "missing",
431
+ errors: [message]
432
+ }));
433
+ }
434
+ return expected.map((assertion) => evaluateHttpStatusBodyJsonAssertion(parsed, assertion));
435
+ }
283
436
  function compactProfileSetupSummaryText(value, limit = 160) {
284
437
  const text = typeof value === "string" ? value.replace(/\s+/g, " ").trim() : "";
285
438
  if (!text) return void 0;
@@ -934,6 +1087,46 @@ function validateRegexPatterns(patterns, label) {
934
1087
  }
935
1088
  }
936
1089
  }
1090
+ function normalizeHttpStatusBodyJsonAssertions(value, label) {
1091
+ if (value === void 0) return void 0;
1092
+ if (!Array.isArray(value)) throw new Error(`${label} must be an array.`);
1093
+ if (!value.length) throw new Error(`${label} must not be empty.`);
1094
+ return value.map((item, index) => {
1095
+ const itemLabel = `${label}[${index}]`;
1096
+ if (typeof item === "string") {
1097
+ const path2 = stringValue(item);
1098
+ if (!path2) throw new Error(`${itemLabel} path must not be empty.`);
1099
+ return { path: path2, exists: true };
1100
+ }
1101
+ if (!isRecord(item)) throw new Error(`${itemLabel} must be an object or JSON path string.`);
1102
+ const path = stringFromOwn(item, "path", "json_path", "jsonPath", "key");
1103
+ if (!path) throw new Error(`${itemLabel}.path is required.`);
1104
+ const assertion = {
1105
+ label: stringValue(item.label),
1106
+ path
1107
+ };
1108
+ const exists = booleanValue(valueFromOwn(item, "exists", "present"));
1109
+ if (exists !== void 0) assertion.exists = exists;
1110
+ const type = stringValue(valueFromOwn(item, "type", "value_type", "valueType"));
1111
+ if (type !== void 0) {
1112
+ const allowedTypes = ["array", "boolean", "null", "number", "object", "string"];
1113
+ if (!allowedTypes.includes(type)) {
1114
+ throw new Error(`${itemLabel}.type must be one of ${allowedTypes.join(", ")}.`);
1115
+ }
1116
+ assertion.type = type;
1117
+ }
1118
+ const equalsValue = valueFromOwn(item, "equals", "expected", "expected_value", "expectedValue", "value");
1119
+ if (equalsValue !== void 0) assertion.equals = toJsonValue(equalsValue);
1120
+ const notEqualsValue = valueFromOwn(item, "not_equals", "notEquals", "forbidden", "forbidden_value", "forbiddenValue");
1121
+ if (notEqualsValue !== void 0) assertion.not_equals = toJsonValue(notEqualsValue);
1122
+ const containsValue = valueFromOwn(item, "contains", "includes", "contains_value", "containsValue", "include");
1123
+ if (containsValue !== void 0) assertion.contains = toJsonValue(containsValue);
1124
+ if (assertion.exists === void 0 && assertion.type === void 0 && !hasOwn(assertion, "equals") && !hasOwn(assertion, "not_equals") && !hasOwn(assertion, "contains")) {
1125
+ assertion.exists = true;
1126
+ }
1127
+ return assertion;
1128
+ });
1129
+ }
937
1130
  function isDialogCountCheckType(type) {
938
1131
  return type === "dialog_count_equals" || type === "dialog_accept_count_equals" || type === "dialog_dismiss_count_equals";
939
1132
  }
@@ -1033,6 +1226,10 @@ function normalizeCheck(input, index) {
1033
1226
  `checks[${index}] body_not_patterns`
1034
1227
  ) : void 0;
1035
1228
  if (bodyNotPatterns?.length) validateRegexPatterns(bodyNotPatterns, `checks[${index}] body_not_patterns`);
1229
+ const bodyJsonAssertions = isHttpStatusCheck ? normalizeHttpStatusBodyJsonAssertions(
1230
+ input.body_json_assertions ?? input.bodyJsonAssertions ?? input.json_body_assertions ?? input.jsonBodyAssertions ?? input.json_assertions ?? input.jsonAssertions ?? input.response_json_assertions ?? input.responseJsonAssertions,
1231
+ `checks[${index}] body_json_assertions`
1232
+ ) : void 0;
1036
1233
  if (isLinkStatusCheck) {
1037
1234
  if (minCount !== void 0 && (!Number.isInteger(minCount) || minCount < 0)) {
1038
1235
  throw new Error(`checks[${index}] ${type} min_count must be a non-negative integer.`);
@@ -1058,6 +1255,7 @@ function normalizeCheck(input, index) {
1058
1255
  body_contains: bodyContains,
1059
1256
  body_not_contains: bodyNotContains,
1060
1257
  body_not_patterns: bodyNotPatterns,
1258
+ body_json_assertions: bodyJsonAssertions,
1061
1259
  expected_texts: expectedTexts,
1062
1260
  link_selector: stringValue(input.link_selector) || stringValue(input.linkSelector),
1063
1261
  source_selector: stringValue(input.source_selector) || stringValue(input.sourceSelector),
@@ -1264,6 +1462,34 @@ function httpStatusBodyNotPatternFailures(result, check) {
1264
1462
  const observed = isRecord(result.body_not_patterns) ? result.body_not_patterns : {};
1265
1463
  return forbidden.filter((pattern) => observed[pattern] !== false);
1266
1464
  }
1465
+ function httpStatusBodyJsonAssertionFailures(result, check) {
1466
+ const expected = check.body_json_assertions?.filter((assertion) => assertion.path) ?? [];
1467
+ if (!expected.length) return [];
1468
+ if (!Array.isArray(result.body_json_assertions)) {
1469
+ return expected.map((assertion) => ({
1470
+ label: assertion.label || assertion.path,
1471
+ path: assertion.path,
1472
+ ok: false,
1473
+ exists: false,
1474
+ observed_type: "missing",
1475
+ errors: ["body_json_assertions evidence missing"]
1476
+ }));
1477
+ }
1478
+ return result.body_json_assertions.filter((assertion) => isRecord(assertion) && assertion.ok !== true).map((assertion) => ({
1479
+ label: stringValue(assertion.label) || stringValue(assertion.path) || "json assertion",
1480
+ path: stringValue(assertion.path) || "",
1481
+ ok: false,
1482
+ exists: assertion.exists === true,
1483
+ observed: hasOwn(assertion, "observed") ? toJsonValue(assertion.observed) : void 0,
1484
+ observed_type: stringValue(assertion.observed_type) || "missing",
1485
+ expected_exists: booleanValue(assertion.expected_exists),
1486
+ equals: hasOwn(assertion, "equals") ? toJsonValue(assertion.equals) : void 0,
1487
+ not_equals: hasOwn(assertion, "not_equals") ? toJsonValue(assertion.not_equals) : void 0,
1488
+ contains: hasOwn(assertion, "contains") ? toJsonValue(assertion.contains) : void 0,
1489
+ type: stringValue(assertion.type),
1490
+ errors: Array.isArray(assertion.errors) ? assertion.errors.map(String) : void 0
1491
+ }));
1492
+ }
1267
1493
  function linkStatusResultOk(result, check) {
1268
1494
  const status = numberValue(result.status);
1269
1495
  if (!httpStatusIsAllowed(status, check)) return false;
@@ -1281,6 +1507,7 @@ function linkStatusResultOk(result, check) {
1281
1507
  if (httpStatusBodyContainsFailures(result, check).length) return false;
1282
1508
  if (httpStatusBodyNotContainsFailures(result, check).length) return false;
1283
1509
  if (httpStatusBodyNotPatternFailures(result, check).length) return false;
1510
+ if (httpStatusBodyJsonAssertionFailures(result, check).length) return false;
1284
1511
  return true;
1285
1512
  }
1286
1513
  function responseHeader(response, name) {
@@ -1349,7 +1576,7 @@ async function preflightHttpStatusCheck(check, index, targetUrl, fetchImpl) {
1349
1576
  statusText = typeof response.statusText === "string" ? response.statusText : "";
1350
1577
  result.content_type = responseHeader(response, "content-type");
1351
1578
  result.content_length = responseContentLength(response);
1352
- 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);
1579
+ 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);
1353
1580
  if (shouldReadBody && method !== "HEAD") {
1354
1581
  const body = await responseBodyText(response);
1355
1582
  result.bytes = body.bytes;
@@ -1362,6 +1589,9 @@ async function preflightHttpStatusCheck(check, index, targetUrl, fetchImpl) {
1362
1589
  if (check.body_not_patterns?.length) {
1363
1590
  result.body_not_patterns = Object.fromEntries(check.body_not_patterns.filter(Boolean).map((pattern) => [pattern, new RegExp(pattern).test(body.text)]));
1364
1591
  }
1592
+ if (check.body_json_assertions?.length) {
1593
+ result.body_json_assertions = evaluateHttpStatusBodyJsonAssertions(body.text, check.body_json_assertions);
1594
+ }
1365
1595
  }
1366
1596
  } catch (caught) {
1367
1597
  error = String(caught instanceof Error ? caught.message : caught).slice(0, 500);
@@ -1370,6 +1600,7 @@ async function preflightHttpStatusCheck(check, index, targetUrl, fetchImpl) {
1370
1600
  const bodyContainsMissing = httpStatusBodyContainsFailures(result, check);
1371
1601
  const bodyNotContainsFound = httpStatusBodyNotContainsFailures(result, check);
1372
1602
  const bodyNotPatternsFound = httpStatusBodyNotPatternFailures(result, check);
1603
+ const bodyJsonAssertionsFailed = httpStatusBodyJsonAssertionFailures(result, check);
1373
1604
  const ok = !error && linkStatusResultOk(result, check);
1374
1605
  return {
1375
1606
  index,
@@ -1388,7 +1619,9 @@ async function preflightHttpStatusCheck(check, index, targetUrl, fetchImpl) {
1388
1619
  body_not_contains: isRecord(result.body_not_contains) ? Object.fromEntries(Object.entries(result.body_not_contains).map(([key, value]) => [key, value === true])) : null,
1389
1620
  body_not_contains_found: bodyNotContainsFound,
1390
1621
  body_not_patterns: isRecord(result.body_not_patterns) ? Object.fromEntries(Object.entries(result.body_not_patterns).map(([key, value]) => [key, value === true])) : null,
1391
- body_not_patterns_found: bodyNotPatternsFound
1622
+ body_not_patterns_found: bodyNotPatternsFound,
1623
+ body_json_assertions: Array.isArray(result.body_json_assertions) ? result.body_json_assertions : null,
1624
+ body_json_assertions_failed: bodyJsonAssertionsFailed
1392
1625
  };
1393
1626
  }
1394
1627
  async function preflightRiddleProofProfileHttpStatusChecks(profile, options = {}) {
@@ -1433,6 +1666,7 @@ function summarizeHttpStatusEvidence(viewport, check) {
1433
1666
  const bodyContainsMissing = httpStatusBodyContainsFailures(statusEvidence, check);
1434
1667
  const bodyNotContainsFound = httpStatusBodyNotContainsFailures(statusEvidence, check);
1435
1668
  const bodyNotPatternsFound = httpStatusBodyNotPatternFailures(statusEvidence, check);
1669
+ const bodyJsonAssertionsFailed = httpStatusBodyJsonAssertionFailures(statusEvidence, check);
1436
1670
  if (!linkStatusResultOk(statusEvidence, check)) {
1437
1671
  failures.push({
1438
1672
  code: "http_status_failed",
@@ -1451,6 +1685,8 @@ function summarizeHttpStatusEvidence(viewport, check) {
1451
1685
  body_not_contains_found: bodyNotContainsFound,
1452
1686
  body_not_patterns: check.body_not_patterns ?? null,
1453
1687
  body_not_patterns_found: bodyNotPatternsFound,
1688
+ body_json_assertions: check.body_json_assertions ?? null,
1689
+ body_json_assertions_failed: bodyJsonAssertionsFailed.map((assertion) => toJsonValue(assertion)),
1454
1690
  body_sample: stringValue(statusEvidence.body_sample) ?? null
1455
1691
  });
1456
1692
  }
@@ -1472,6 +1708,8 @@ function summarizeHttpStatusEvidence(viewport, check) {
1472
1708
  body_not_contains_found: bodyNotContainsFound,
1473
1709
  body_not_patterns: isRecord(statusEvidence.body_not_patterns) ? toJsonValue(statusEvidence.body_not_patterns) : null,
1474
1710
  body_not_patterns_found: bodyNotPatternsFound,
1711
+ body_json_assertions: Array.isArray(statusEvidence.body_json_assertions) ? toJsonValue(statusEvidence.body_json_assertions) : null,
1712
+ body_json_assertions_failed: bodyJsonAssertionsFailed.map((assertion) => toJsonValue(assertion)),
1475
1713
  body_sample: stringValue(statusEvidence.body_sample) ?? null,
1476
1714
  failures
1477
1715
  };
@@ -2108,6 +2346,7 @@ function assessCheckFromEvidence(check, evidence) {
2108
2346
  body_contains: check.body_contains ?? [],
2109
2347
  body_not_contains: check.body_not_contains ?? [],
2110
2348
  body_not_patterns: check.body_not_patterns ?? [],
2349
+ body_json_assertions: toJsonValue(check.body_json_assertions ?? []),
2111
2350
  viewports: summaries.map((summary) => toJsonValue(summary)),
2112
2351
  failures: failed.flatMap((summary) => Array.isArray(summary.failures) ? summary.failures.map((failure) => toJsonValue({ viewport: stringValue(summary.viewport) ?? null, failure })) : [])
2113
2352
  },
@@ -2851,6 +3090,36 @@ function httpStatusBodyNotPatternFailures(result, check) {
2851
3090
  : {};
2852
3091
  return forbidden.filter((pattern) => observed[pattern] !== false);
2853
3092
  }
3093
+ function httpStatusBodyJsonAssertionFailures(result, check) {
3094
+ const expected = Array.isArray(check.body_json_assertions) ? check.body_json_assertions.filter((assertion) => assertion && assertion.path) : [];
3095
+ if (!expected.length) return [];
3096
+ if (!Array.isArray(result.body_json_assertions)) {
3097
+ return expected.map((assertion) => ({
3098
+ label: assertion.label || assertion.path,
3099
+ path: assertion.path,
3100
+ ok: false,
3101
+ exists: false,
3102
+ observed_type: "missing",
3103
+ errors: ["body_json_assertions evidence missing"],
3104
+ }));
3105
+ }
3106
+ return result.body_json_assertions
3107
+ .filter((assertion) => assertion && typeof assertion === "object" && assertion.ok !== true)
3108
+ .map((assertion) => ({
3109
+ label: typeof assertion.label === "string" && assertion.label ? assertion.label : typeof assertion.path === "string" && assertion.path ? assertion.path : "json assertion",
3110
+ path: typeof assertion.path === "string" ? assertion.path : "",
3111
+ ok: false,
3112
+ exists: assertion.exists === true,
3113
+ observed: Object.hasOwn(assertion, "observed") ? assertion.observed : undefined,
3114
+ observed_type: typeof assertion.observed_type === "string" && assertion.observed_type ? assertion.observed_type : "missing",
3115
+ expected_exists: typeof assertion.expected_exists === "boolean" ? assertion.expected_exists : undefined,
3116
+ equals: Object.hasOwn(assertion, "equals") ? assertion.equals : undefined,
3117
+ not_equals: Object.hasOwn(assertion, "not_equals") ? assertion.not_equals : undefined,
3118
+ contains: Object.hasOwn(assertion, "contains") ? assertion.contains : undefined,
3119
+ type: typeof assertion.type === "string" ? assertion.type : undefined,
3120
+ errors: Array.isArray(assertion.errors) ? assertion.errors.map(String) : undefined,
3121
+ }));
3122
+ }
2854
3123
  function linkStatusResultOk(result, check) {
2855
3124
  if (!result || typeof result !== "object" || Array.isArray(result)) return false;
2856
3125
  if (!httpStatusIsAllowed(result.status, check)) return false;
@@ -2868,6 +3137,7 @@ function linkStatusResultOk(result, check) {
2868
3137
  if (httpStatusBodyContainsFailures(result, check).length) return false;
2869
3138
  if (httpStatusBodyNotContainsFailures(result, check).length) return false;
2870
3139
  if (httpStatusBodyNotPatternFailures(result, check).length) return false;
3140
+ if (httpStatusBodyJsonAssertionFailures(result, check).length) return false;
2871
3141
  return true;
2872
3142
  }
2873
3143
  function summarizeHttpStatusEvidence(viewport, check) {
@@ -2888,6 +3158,7 @@ function summarizeHttpStatusEvidence(viewport, check) {
2888
3158
  const bodyContainsMissing = httpStatusBodyContainsFailures(statusEvidence, check);
2889
3159
  const bodyNotContainsFound = httpStatusBodyNotContainsFailures(statusEvidence, check);
2890
3160
  const bodyNotPatternsFound = httpStatusBodyNotPatternFailures(statusEvidence, check);
3161
+ const bodyJsonAssertionsFailed = httpStatusBodyJsonAssertionFailures(statusEvidence, check);
2891
3162
  if (!linkStatusResultOk(statusEvidence, check)) {
2892
3163
  failures.push({
2893
3164
  code: "http_status_failed",
@@ -2906,6 +3177,8 @@ function summarizeHttpStatusEvidence(viewport, check) {
2906
3177
  body_not_contains_found: bodyNotContainsFound,
2907
3178
  body_not_patterns: Array.isArray(check.body_not_patterns) ? check.body_not_patterns : null,
2908
3179
  body_not_patterns_found: bodyNotPatternsFound,
3180
+ body_json_assertions: Array.isArray(check.body_json_assertions) ? check.body_json_assertions : null,
3181
+ body_json_assertions_failed: bodyJsonAssertionsFailed,
2909
3182
  body_sample: typeof statusEvidence.body_sample === "string" ? statusEvidence.body_sample : null,
2910
3183
  });
2911
3184
  }
@@ -2933,6 +3206,8 @@ function summarizeHttpStatusEvidence(viewport, check) {
2933
3206
  ? statusEvidence.body_not_patterns
2934
3207
  : null,
2935
3208
  body_not_patterns_found: bodyNotPatternsFound,
3209
+ body_json_assertions: Array.isArray(statusEvidence.body_json_assertions) ? statusEvidence.body_json_assertions : null,
3210
+ body_json_assertions_failed: bodyJsonAssertionsFailed,
2936
3211
  body_sample: typeof statusEvidence.body_sample === "string" ? statusEvidence.body_sample : null,
2937
3212
  failures,
2938
3213
  };
@@ -5035,6 +5310,155 @@ function linkProbeResponseFields(response, method) {
5035
5310
  content_length: contentLength,
5036
5311
  };
5037
5312
  }
5313
+ function jsonProbeValueType(value) {
5314
+ if (value === null) return "null";
5315
+ if (Array.isArray(value)) return "array";
5316
+ if (typeof value === "boolean") return "boolean";
5317
+ if (typeof value === "number") return "number";
5318
+ if (typeof value === "string") return "string";
5319
+ return "object";
5320
+ }
5321
+ function jsonProbeDeepEqual(left, right) {
5322
+ if (left === right) return true;
5323
+ if (typeof left !== typeof right) return false;
5324
+ if (left === null || right === null) return left === right;
5325
+ if (typeof left !== "object" || typeof right !== "object") return false;
5326
+ if (Array.isArray(left) || Array.isArray(right)) {
5327
+ if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) return false;
5328
+ return left.every((item, index) => jsonProbeDeepEqual(item, right[index]));
5329
+ }
5330
+ const leftKeys = Object.keys(left).sort();
5331
+ const rightKeys = Object.keys(right).sort();
5332
+ if (!jsonProbeDeepEqual(leftKeys, rightKeys)) return false;
5333
+ return leftKeys.every((key) => jsonProbeDeepEqual(left[key], right[key]));
5334
+ }
5335
+ function jsonProbeContains(observed, expected) {
5336
+ if (typeof observed === "string" && typeof expected === "string") return observed.includes(expected);
5337
+ if (Array.isArray(observed)) return observed.some((item) => jsonProbeDeepEqual(item, expected));
5338
+ if (observed && expected && typeof observed === "object" && typeof expected === "object" && !Array.isArray(observed) && !Array.isArray(expected)) {
5339
+ return Object.entries(expected).every(([key, value]) => Object.hasOwn(observed, key) && jsonProbeDeepEqual(observed[key], value));
5340
+ }
5341
+ return false;
5342
+ }
5343
+ function parseJsonProbePathSegments(path) {
5344
+ let input = String(path || "").trim();
5345
+ if (!input) throw new Error("path is empty");
5346
+ if (input === "$") return [];
5347
+ if (input.startsWith("$.")) input = input.slice(2);
5348
+ else if (input.startsWith("$[")) input = input.slice(1);
5349
+ const segments = [];
5350
+ let token = "";
5351
+ const pushToken = () => {
5352
+ if (!token) return;
5353
+ segments.push(token);
5354
+ token = "";
5355
+ };
5356
+ for (let index = 0; index < input.length; index += 1) {
5357
+ const char = input[index];
5358
+ if (char === ".") {
5359
+ pushToken();
5360
+ continue;
5361
+ }
5362
+ if (char !== "[") {
5363
+ token += char;
5364
+ continue;
5365
+ }
5366
+ pushToken();
5367
+ const closeIndex = input.indexOf("]", index + 1);
5368
+ if (closeIndex === -1) throw new Error("unterminated bracket at " + index);
5369
+ const bracket = input.slice(index + 1, closeIndex).trim();
5370
+ if (!bracket) throw new Error("empty bracket at " + index);
5371
+ if (/^\d+$/.test(bracket)) {
5372
+ segments.push(Number(bracket));
5373
+ } else if ((bracket.startsWith('"') && bracket.endsWith('"')) || (bracket.startsWith("'") && bracket.endsWith("'"))) {
5374
+ const quoted = bracket.startsWith("'")
5375
+ ? '"' + bracket.slice(1, -1).replace(/\\/g, "\\\\").replace(/"/g, "\\\"") + '"'
5376
+ : bracket;
5377
+ segments.push(String(JSON.parse(quoted)));
5378
+ } else {
5379
+ segments.push(bracket);
5380
+ }
5381
+ index = closeIndex;
5382
+ }
5383
+ pushToken();
5384
+ return segments;
5385
+ }
5386
+ function resolveJsonProbePath(root, path) {
5387
+ let segments;
5388
+ try {
5389
+ segments = parseJsonProbePathSegments(path);
5390
+ } catch (error) {
5391
+ return { exists: false, error: String(error && error.message ? error.message : error) };
5392
+ }
5393
+ let current = root;
5394
+ for (const segment of segments) {
5395
+ if (typeof segment === "number") {
5396
+ if (!Array.isArray(current) || segment < 0 || segment >= current.length) return { exists: false };
5397
+ current = current[segment];
5398
+ continue;
5399
+ }
5400
+ if (!current || typeof current !== "object" || Array.isArray(current) || !Object.hasOwn(current, segment)) {
5401
+ return { exists: false };
5402
+ }
5403
+ current = current[segment];
5404
+ }
5405
+ return { exists: true, value: current };
5406
+ }
5407
+ function evaluateJsonProbeAssertion(root, assertion) {
5408
+ const resolved = resolveJsonProbePath(root, assertion.path);
5409
+ const errors = [];
5410
+ const result = {
5411
+ label: assertion.label || assertion.path,
5412
+ path: assertion.path,
5413
+ ok: true,
5414
+ exists: resolved.exists,
5415
+ observed_type: resolved.exists ? jsonProbeValueType(resolved.value) : "missing",
5416
+ };
5417
+ if (resolved.exists) result.observed = resolved.value;
5418
+ if (resolved.error) errors.push(resolved.error);
5419
+ if (Object.hasOwn(assertion, "exists")) {
5420
+ result.expected_exists = assertion.exists;
5421
+ if (resolved.exists !== assertion.exists) errors.push("expected exists=" + assertion.exists);
5422
+ }
5423
+ if (Object.hasOwn(assertion, "type")) {
5424
+ result.type = assertion.type;
5425
+ if (!resolved.exists || jsonProbeValueType(resolved.value) !== assertion.type) errors.push("expected type " + assertion.type);
5426
+ }
5427
+ if (Object.hasOwn(assertion, "equals")) {
5428
+ result.equals = assertion.equals;
5429
+ if (!resolved.exists || !jsonProbeDeepEqual(resolved.value, assertion.equals)) errors.push("expected JSON value equality");
5430
+ }
5431
+ if (Object.hasOwn(assertion, "not_equals")) {
5432
+ result.not_equals = assertion.not_equals;
5433
+ if (resolved.exists && jsonProbeDeepEqual(resolved.value, assertion.not_equals)) errors.push("expected JSON value inequality");
5434
+ }
5435
+ if (Object.hasOwn(assertion, "contains")) {
5436
+ result.contains = assertion.contains;
5437
+ if (!resolved.exists || !jsonProbeContains(resolved.value, assertion.contains)) errors.push("expected JSON value containment");
5438
+ }
5439
+ result.ok = errors.length === 0;
5440
+ if (errors.length) result.errors = errors;
5441
+ return result;
5442
+ }
5443
+ function evaluateJsonProbeAssertions(text, assertions) {
5444
+ const expected = Array.isArray(assertions) ? assertions.filter((assertion) => assertion && assertion.path) : [];
5445
+ if (!expected.length) return [];
5446
+ let parsed;
5447
+ try {
5448
+ parsed = JSON.parse(text);
5449
+ } catch (error) {
5450
+ const message = "response body is not valid JSON: " + String(error && error.message ? error.message : error).slice(0, 200);
5451
+ return expected.map((assertion) => ({
5452
+ label: assertion.label || assertion.path,
5453
+ path: assertion.path,
5454
+ ok: false,
5455
+ exists: false,
5456
+ observed_type: "missing",
5457
+ errors: [message],
5458
+ }));
5459
+ }
5460
+ return expected.map((assertion) => evaluateJsonProbeAssertion(parsed, assertion));
5461
+ }
5038
5462
  async function collectHttpStatus(check) {
5039
5463
  const url = httpStatusRequestUrl(check, page.url() || targetUrl);
5040
5464
  const method = httpStatusMethod(check);
@@ -5051,6 +5475,7 @@ async function collectHttpStatus(check) {
5051
5475
  const bodyContains = Array.isArray(check.body_contains) ? check.body_contains.filter(Boolean) : [];
5052
5476
  const bodyNotContains = Array.isArray(check.body_not_contains) ? check.body_not_contains.filter(Boolean) : [];
5053
5477
  const bodyNotPatterns = Array.isArray(check.body_not_patterns) ? check.body_not_patterns.filter(Boolean) : [];
5478
+ const bodyJsonAssertions = Array.isArray(check.body_json_assertions) ? check.body_json_assertions.filter((assertion) => assertion && assertion.path) : [];
5054
5479
  const options = {
5055
5480
  method,
5056
5481
  redirect: "follow",
@@ -5076,17 +5501,18 @@ async function collectHttpStatus(check) {
5076
5501
  Object.assign(result, linkProbeResponseFields(response, method));
5077
5502
  result.url = url;
5078
5503
  result.status_text = response.statusText || "";
5079
- 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;
5504
+ 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;
5080
5505
  if (shouldReadBody) {
5081
5506
  try {
5082
5507
  const buffer = await response.arrayBuffer();
5083
5508
  result.bytes = buffer.byteLength;
5084
- if (bodyContains.length || bodyNotContains.length || bodyNotPatterns.length) {
5509
+ if (bodyContains.length || bodyNotContains.length || bodyNotPatterns.length || bodyJsonAssertions.length) {
5085
5510
  const text = new TextDecoder().decode(buffer);
5086
5511
  result.body_sample = text.slice(0, 1000);
5087
5512
  if (bodyContains.length) result.body_contains = Object.fromEntries(bodyContains.map((expected) => [expected, text.includes(expected)]));
5088
5513
  if (bodyNotContains.length) result.body_not_contains = Object.fromEntries(bodyNotContains.map((forbidden) => [forbidden, text.includes(forbidden)]));
5089
5514
  if (bodyNotPatterns.length) result.body_not_patterns = Object.fromEntries(bodyNotPatterns.map((pattern) => [pattern, new RegExp(pattern).test(text)]));
5515
+ if (bodyJsonAssertions.length) result.body_json_assertions = evaluateJsonProbeAssertions(text, bodyJsonAssertions);
5090
5516
  }
5091
5517
  } catch (error) {
5092
5518
  result.error = String(error && error.message ? error.message : error).slice(0, 500);
@@ -5099,6 +5525,7 @@ async function collectHttpStatus(check) {
5099
5525
  && (!bodyContains.length || bodyContains.every((expected) => result.body_contains && result.body_contains[expected] === true))
5100
5526
  && (!bodyNotContains.length || bodyNotContains.every((forbidden) => result.body_not_contains && result.body_not_contains[forbidden] === false))
5101
5527
  && (!bodyNotPatterns.length || bodyNotPatterns.every((pattern) => result.body_not_patterns && result.body_not_patterns[pattern] === false))
5528
+ && (!bodyJsonAssertions.length || (Array.isArray(result.body_json_assertions) && result.body_json_assertions.every((assertion) => assertion.ok === true)))
5102
5529
  && !result.error;
5103
5530
  return result;
5104
5531
  } catch (error) {