@riddledc/riddle-proof 0.7.94 → 0.7.95

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/README.md CHANGED
@@ -479,6 +479,8 @@ artifact links should fail the profile:
479
479
  "expected_count": 88,
480
480
  "same_origin_only": true,
481
481
  "require_nonzero_bytes": true,
482
+ "min_bytes": 32,
483
+ "allowed_content_types": ["image/*", "application/json"],
482
484
  "max_links": 150
483
485
  }
484
486
  ```
@@ -486,9 +488,12 @@ artifact links should fail the profile:
486
488
  The check defaults to `a[href]`, deduplicates URLs, probes up to 100 selected
487
489
  URLs, and treats HTTP `2xx` / `3xx` responses as healthy. Use
488
490
  `allowed_statuses` or `expected_status` when a narrower status contract is
489
- intentional, `min_count` for lower-bound audits, and `max_links` when the
490
- selected set is intentionally larger than 100. `artifact_link_status` is an
491
- alias with the same behavior for profiles that want artifact-specific wording.
491
+ intentional, `min_count` for lower-bound audits, `min_bytes` when a one-byte
492
+ range response is too weak, `allowed_content_types` for MIME checks, and
493
+ `max_links` when the selected set is intentionally larger than 100.
494
+ `allowed_content_types` accepts exact types and family wildcards such as
495
+ `image/*`. `artifact_link_status` is an alias with the same behavior for
496
+ profiles that want artifact-specific wording.
492
497
 
493
498
  Use the `route_inventory` check for source-page route coverage audits where a
494
499
  navigation surface must expose a known set of routes and each route must load
@@ -785,6 +785,12 @@ function normalizeCheck(input, index) {
785
785
  `checks[${index}] allowed_statuses`
786
786
  ) : void 0;
787
787
  const maxLinks = isLinkStatusCheck ? normalizePositiveInteger(input.max_links ?? input.maxLinks ?? input.limit, `checks[${index}] max_links`, 500) : void 0;
788
+ const minBytes = isLinkStatusCheck ? normalizePositiveInteger(input.min_bytes ?? input.minBytes ?? input.min_response_bytes ?? input.minResponseBytes, `checks[${index}] min_bytes`) : void 0;
789
+ const expectedContentType = isLinkStatusCheck ? stringValue(input.expected_content_type) || stringValue(input.expectedContentType) || stringValue(input.content_type) || stringValue(input.contentType) : void 0;
790
+ const allowedContentTypes = isLinkStatusCheck ? normalizeStringList(
791
+ input.allowed_content_types ?? input.allowedContentTypes ?? input.expected_content_types ?? input.expectedContentTypes,
792
+ `checks[${index}] allowed_content_types`
793
+ ) ?? (expectedContentType ? [expectedContentType] : void 0) : void 0;
788
794
  if (isLinkStatusCheck) {
789
795
  if (minCount !== void 0 && (!Number.isInteger(minCount) || minCount < 0)) {
790
796
  throw new Error(`checks[${index}] ${type} min_count must be a non-negative integer.`);
@@ -826,6 +832,8 @@ function normalizeCheck(input, index) {
826
832
  same_origin_only: isLinkStatusCheck ? input.same_origin_only === true || input.sameOriginOnly === true : void 0,
827
833
  dedupe: isLinkStatusCheck ? input.dedupe === false ? false : true : void 0,
828
834
  require_nonzero_bytes: isLinkStatusCheck ? input.require_nonzero_bytes === true || input.requireNonzeroBytes === true : void 0,
835
+ min_bytes: minBytes,
836
+ allowed_content_types: allowedContentTypes,
829
837
  allow_get_fallback: isLinkStatusCheck ? input.allow_get_fallback === false || input.allowGetFallback === false ? false : true : void 0,
830
838
  max_overflow_px: numberValue(input.max_overflow_px),
831
839
  timeout_ms: numberValue(input.timeout_ms) ?? numberValue(input.timeoutMs),
@@ -932,15 +940,40 @@ function linkStatusIsAllowed(status, check) {
932
940
  const allowed = linkStatusAllowedStatuses(check);
933
941
  return allowed?.length ? allowed.includes(status) : status >= 200 && status < 400;
934
942
  }
943
+ function linkStatusObservedBytes(result) {
944
+ const bytes = numberValue(result.bytes);
945
+ const contentLength = numberValue(result.content_length);
946
+ return Math.max(bytes ?? 0, contentLength ?? 0) || void 0;
947
+ }
948
+ function normalizeLinkStatusContentType(value) {
949
+ const normalized = value?.split(";")[0]?.trim().toLowerCase();
950
+ return normalized || void 0;
951
+ }
952
+ function linkStatusContentTypeOk(result, check) {
953
+ const expected = check.allowed_content_types;
954
+ if (!expected?.length) return true;
955
+ const actual = normalizeLinkStatusContentType(stringValue(result.content_type));
956
+ if (!actual) return false;
957
+ return expected.some((contentType) => {
958
+ const normalized = normalizeLinkStatusContentType(contentType);
959
+ if (!normalized) return false;
960
+ if (normalized.endsWith("/*")) return actual.startsWith(normalized.slice(0, -1));
961
+ return actual === normalized;
962
+ });
963
+ }
935
964
  function linkStatusResultOk(result, check) {
936
965
  const status = numberValue(result.status);
937
966
  if (!linkStatusIsAllowed(status, check)) return false;
938
967
  if (stringValue(result.error)) return false;
939
968
  if (result.ok === false) return false;
969
+ if (!linkStatusContentTypeOk(result, check)) return false;
940
970
  if (check.require_nonzero_bytes) {
941
- const bytes = numberValue(result.bytes);
942
- const contentLength = numberValue(result.content_length);
943
- return bytes !== void 0 && bytes > 0 || contentLength !== void 0 && contentLength > 0;
971
+ const observedBytes = linkStatusObservedBytes(result);
972
+ if (observedBytes === void 0 || observedBytes <= 0) return false;
973
+ }
974
+ if (check.min_bytes !== void 0) {
975
+ const observedBytes = linkStatusObservedBytes(result);
976
+ if (observedBytes === void 0 || observedBytes < check.min_bytes) return false;
944
977
  }
945
978
  return true;
946
979
  }
@@ -969,7 +1002,10 @@ function summarizeLinkStatusEvidence(viewport, check) {
969
1002
  status: numberValue(result.status) ?? null,
970
1003
  method: stringValue(result.method) ?? null,
971
1004
  error: stringValue(result.error) ?? null,
972
- bytes: numberValue(result.bytes) ?? numberValue(result.content_length) ?? null
1005
+ content_type: stringValue(result.content_type) ?? null,
1006
+ bytes: linkStatusObservedBytes(result) ?? null,
1007
+ min_bytes: check.min_bytes ?? null,
1008
+ allowed_content_types: check.allowed_content_types ?? null
973
1009
  }));
974
1010
  if (stringValue(linkEvidence.error)) {
975
1011
  failures.push({ code: "link_status_capture_failed", error: stringValue(linkEvidence.error) ?? "" });
@@ -1006,6 +1042,8 @@ function summarizeLinkStatusEvidence(viewport, check) {
1006
1042
  failed_count: failures.length,
1007
1043
  truncated: linkEvidence.truncated === true,
1008
1044
  max_links: numberValue(linkEvidence.max_links) ?? check.max_links ?? 100,
1045
+ min_bytes: check.min_bytes ?? null,
1046
+ allowed_content_types: check.allowed_content_types ?? null,
1009
1047
  status_counts: statusCounts,
1010
1048
  failures: failures.slice(0, 20)
1011
1049
  };
@@ -1534,6 +1572,8 @@ function assessCheckFromEvidence(check, evidence) {
1534
1572
  min_count: check.min_count ?? null,
1535
1573
  allowed_statuses: linkStatusAllowedStatuses(check) || ["2xx", "3xx"],
1536
1574
  require_nonzero_bytes: check.require_nonzero_bytes === true,
1575
+ min_bytes: check.min_bytes ?? null,
1576
+ allowed_content_types: check.allowed_content_types ?? null,
1537
1577
  viewports: summaries.map((summary) => toJsonValue(summary)),
1538
1578
  failures: failed.flatMap((summary) => Array.isArray(summary.failures) ? summary.failures.map((failure) => toJsonValue({ viewport: stringValue(summary.viewport) ?? null, failure })) : [])
1539
1579
  },
@@ -2137,15 +2177,41 @@ function linkStatusIsAllowed(status, check) {
2137
2177
  const allowed = linkStatusAllowedStatuses(check);
2138
2178
  return Array.isArray(allowed) && allowed.length ? allowed.includes(status) : status >= 200 && status < 400;
2139
2179
  }
2180
+ function linkStatusObservedBytes(result) {
2181
+ const bytes = typeof result.bytes === "number" && Number.isFinite(result.bytes) ? result.bytes : undefined;
2182
+ const contentLength = typeof result.content_length === "number" && Number.isFinite(result.content_length) ? result.content_length : undefined;
2183
+ const observed = Math.max(bytes || 0, contentLength || 0);
2184
+ return observed > 0 ? observed : undefined;
2185
+ }
2186
+ function normalizeLinkStatusContentType(value) {
2187
+ if (typeof value !== "string") return undefined;
2188
+ const normalized = (value.split(";")[0] || "").trim().toLowerCase();
2189
+ return normalized || undefined;
2190
+ }
2191
+ function linkStatusContentTypeOk(result, check) {
2192
+ if (!Array.isArray(check.allowed_content_types) || !check.allowed_content_types.length) return true;
2193
+ const actual = normalizeLinkStatusContentType(result.content_type);
2194
+ if (!actual) return false;
2195
+ return check.allowed_content_types.some((contentType) => {
2196
+ const normalized = normalizeLinkStatusContentType(contentType);
2197
+ if (!normalized) return false;
2198
+ if (normalized.endsWith("/*")) return actual.startsWith(normalized.slice(0, -1));
2199
+ return actual === normalized;
2200
+ });
2201
+ }
2140
2202
  function linkStatusResultOk(result, check) {
2141
2203
  if (!result || typeof result !== "object" || Array.isArray(result)) return false;
2142
2204
  if (!linkStatusIsAllowed(result.status, check)) return false;
2143
2205
  if (typeof result.error === "string" && result.error.trim()) return false;
2144
2206
  if (result.ok === false) return false;
2207
+ if (!linkStatusContentTypeOk(result, check)) return false;
2145
2208
  if (check.require_nonzero_bytes === true) {
2146
- const bytes = typeof result.bytes === "number" && Number.isFinite(result.bytes) ? result.bytes : undefined;
2147
- const contentLength = typeof result.content_length === "number" && Number.isFinite(result.content_length) ? result.content_length : undefined;
2148
- return (bytes !== undefined && bytes > 0) || (contentLength !== undefined && contentLength > 0);
2209
+ const observedBytes = linkStatusObservedBytes(result);
2210
+ if (observedBytes === undefined || observedBytes <= 0) return false;
2211
+ }
2212
+ if (typeof check.min_bytes === "number" && Number.isFinite(check.min_bytes)) {
2213
+ const observedBytes = linkStatusObservedBytes(result);
2214
+ if (observedBytes === undefined || observedBytes < check.min_bytes) return false;
2149
2215
  }
2150
2216
  return true;
2151
2217
  }
@@ -2173,11 +2239,10 @@ function summarizeLinkStatusEvidence(viewport, check) {
2173
2239
  status: typeof result.status === "number" && Number.isFinite(result.status) ? result.status : null,
2174
2240
  method: typeof result.method === "string" ? result.method : null,
2175
2241
  error: typeof result.error === "string" ? result.error : null,
2176
- bytes: typeof result.bytes === "number" && Number.isFinite(result.bytes)
2177
- ? result.bytes
2178
- : typeof result.content_length === "number" && Number.isFinite(result.content_length)
2179
- ? result.content_length
2180
- : null,
2242
+ content_type: typeof result.content_type === "string" ? result.content_type : null,
2243
+ bytes: linkStatusObservedBytes(result) ?? null,
2244
+ min_bytes: typeof check.min_bytes === "number" && Number.isFinite(check.min_bytes) ? check.min_bytes : null,
2245
+ allowed_content_types: Array.isArray(check.allowed_content_types) ? check.allowed_content_types : null,
2181
2246
  }));
2182
2247
  if (typeof linkEvidence.error === "string" && linkEvidence.error.trim()) {
2183
2248
  failures.push({ code: "link_status_capture_failed", error: linkEvidence.error });
@@ -2212,6 +2277,8 @@ function summarizeLinkStatusEvidence(viewport, check) {
2212
2277
  failed_count: failures.length,
2213
2278
  truncated: linkEvidence.truncated === true,
2214
2279
  max_links: typeof linkEvidence.max_links === "number" ? linkEvidence.max_links : check.max_links || 100,
2280
+ min_bytes: typeof check.min_bytes === "number" && Number.isFinite(check.min_bytes) ? check.min_bytes : null,
2281
+ allowed_content_types: Array.isArray(check.allowed_content_types) ? check.allowed_content_types : null,
2215
2282
  status_counts: linkEvidence.status_counts && typeof linkEvidence.status_counts === "object" && !Array.isArray(linkEvidence.status_counts) ? linkEvidence.status_counts : {},
2216
2283
  failures: failures.slice(0, 20),
2217
2284
  };
@@ -2908,6 +2975,8 @@ function assessProfile(profile, evidence) {
2908
2975
  min_count: check.min_count ?? null,
2909
2976
  allowed_statuses: linkStatusAllowedStatuses(check) || ["2xx", "3xx"],
2910
2977
  require_nonzero_bytes: check.require_nonzero_bytes === true,
2978
+ min_bytes: check.min_bytes ?? null,
2979
+ allowed_content_types: check.allowed_content_types ?? null,
2911
2980
  viewports: summaries,
2912
2981
  failures: failed.flatMap((summary) => Array.isArray(summary.failures)
2913
2982
  ? summary.failures.map((failure) => ({ viewport: summary.viewport || null, failure }))
@@ -4055,6 +4124,28 @@ function linkProbeAllowed(status, check) {
4055
4124
  : null;
4056
4125
  return allowed ? allowed.includes(status) : status >= 200 && status < 400;
4057
4126
  }
4127
+ function linkProbeObservedBytes(result) {
4128
+ const bytes = typeof result.bytes === "number" && Number.isFinite(result.bytes) ? result.bytes : undefined;
4129
+ const contentLength = typeof result.content_length === "number" && Number.isFinite(result.content_length) ? result.content_length : undefined;
4130
+ const observed = Math.max(bytes || 0, contentLength || 0);
4131
+ return observed > 0 ? observed : undefined;
4132
+ }
4133
+ function normalizeLinkProbeContentType(value) {
4134
+ if (typeof value !== "string") return undefined;
4135
+ const normalized = (value.split(";")[0] || "").trim().toLowerCase();
4136
+ return normalized || undefined;
4137
+ }
4138
+ function linkProbeContentTypeAllowed(result, check) {
4139
+ if (!Array.isArray(check.allowed_content_types) || !check.allowed_content_types.length) return true;
4140
+ const actual = normalizeLinkProbeContentType(result.content_type);
4141
+ if (!actual) return false;
4142
+ return check.allowed_content_types.some((contentType) => {
4143
+ const normalized = normalizeLinkProbeContentType(contentType);
4144
+ if (!normalized) return false;
4145
+ if (normalized.endsWith("/*")) return actual.startsWith(normalized.slice(0, -1));
4146
+ return actual === normalized;
4147
+ });
4148
+ }
4058
4149
  function linkProbeResponseFields(response, method) {
4059
4150
  const contentLengthHeader = response.headers && typeof response.headers.get === "function" ? response.headers.get("content-length") : null;
4060
4151
  const contentLength = contentLengthHeader && /^\d+$/.test(contentLengthHeader) ? Number(contentLengthHeader) : null;
@@ -4069,6 +4160,7 @@ function linkProbeResponseFields(response, method) {
4069
4160
  }
4070
4161
  async function probeLinkStatus(candidate, check) {
4071
4162
  const requireNonzeroBytes = check.require_nonzero_bytes === true;
4163
+ const minBytes = typeof check.min_bytes === "number" && Number.isFinite(check.min_bytes) ? Math.max(1, Math.floor(check.min_bytes)) : null;
4072
4164
  const allowGetFallback = check.allow_get_fallback !== false;
4073
4165
  const result = {
4074
4166
  url: candidate.url,
@@ -4086,7 +4178,7 @@ async function probeLinkStatus(candidate, check) {
4086
4178
  };
4087
4179
  const applyResponse = async (response, method, readBytes) => {
4088
4180
  Object.assign(result, linkProbeResponseFields(response, method));
4089
- if (readBytes || requireNonzeroBytes) {
4181
+ if (readBytes) {
4090
4182
  try {
4091
4183
  const buffer = await response.arrayBuffer();
4092
4184
  result.bytes = buffer.byteLength;
@@ -4095,7 +4187,9 @@ async function probeLinkStatus(candidate, check) {
4095
4187
  }
4096
4188
  }
4097
4189
  result.ok = linkProbeAllowed(result.status, check)
4098
- && (!requireNonzeroBytes || (typeof result.bytes === "number" ? result.bytes > 0 : typeof result.content_length === "number" && result.content_length > 0))
4190
+ && linkProbeContentTypeAllowed(result, check)
4191
+ && (!requireNonzeroBytes || ((linkProbeObservedBytes(result) || 0) > 0))
4192
+ && (minBytes === null || ((linkProbeObservedBytes(result) || 0) >= minBytes))
4099
4193
  && !result.error;
4100
4194
  };
4101
4195
  try {
@@ -4112,9 +4206,9 @@ async function probeLinkStatus(candidate, check) {
4112
4206
  method: "GET",
4113
4207
  redirect: "follow",
4114
4208
  cache: "no-store",
4115
- headers: { Range: "bytes=0-0" },
4209
+ headers: { Range: "bytes=0-" + String((minBytes || 1) - 1) },
4116
4210
  });
4117
- await applyResponse(response, "GET", requireNonzeroBytes);
4211
+ await applyResponse(response, "GET", requireNonzeroBytes || minBytes !== null);
4118
4212
  return result;
4119
4213
  } catch (error) {
4120
4214
  result.error = String(error && error.message ? error.message : error).slice(0, 500);
@@ -4168,7 +4262,10 @@ async function collectLinkStatus(check) {
4168
4262
  status: result.status,
4169
4263
  method: result.method,
4170
4264
  error: result.error,
4171
- bytes: result.bytes == null ? result.content_length : result.bytes,
4265
+ content_type: result.content_type,
4266
+ bytes: linkProbeObservedBytes(result) || null,
4267
+ min_bytes: typeof check.min_bytes === "number" && Number.isFinite(check.min_bytes) ? check.min_bytes : null,
4268
+ allowed_content_types: Array.isArray(check.allowed_content_types) ? check.allowed_content_types : null,
4172
4269
  }));
4173
4270
  const statusCounts = {};
4174
4271
  for (const result of results) {
@@ -4182,6 +4279,8 @@ async function collectLinkStatus(check) {
4182
4279
  same_origin_only: linkProbeSameOriginOnly(check),
4183
4280
  dedupe: linkProbeDedupe(check),
4184
4281
  require_nonzero_bytes: check.require_nonzero_bytes === true,
4282
+ min_bytes: typeof check.min_bytes === "number" && Number.isFinite(check.min_bytes) ? check.min_bytes : null,
4283
+ allowed_content_types: Array.isArray(check.allowed_content_types) ? check.allowed_content_types : null,
4185
4284
  allowed_statuses: Array.isArray(check.allowed_statuses) && check.allowed_statuses.length
4186
4285
  ? check.allowed_statuses
4187
4286
  : typeof check.expected_status === "number"
package/dist/cli.cjs CHANGED
@@ -7678,6 +7678,12 @@ function normalizeCheck(input, index) {
7678
7678
  `checks[${index}] allowed_statuses`
7679
7679
  ) : void 0;
7680
7680
  const maxLinks = isLinkStatusCheck ? normalizePositiveInteger(input.max_links ?? input.maxLinks ?? input.limit, `checks[${index}] max_links`, 500) : void 0;
7681
+ const minBytes = isLinkStatusCheck ? normalizePositiveInteger(input.min_bytes ?? input.minBytes ?? input.min_response_bytes ?? input.minResponseBytes, `checks[${index}] min_bytes`) : void 0;
7682
+ const expectedContentType = isLinkStatusCheck ? stringValue2(input.expected_content_type) || stringValue2(input.expectedContentType) || stringValue2(input.content_type) || stringValue2(input.contentType) : void 0;
7683
+ const allowedContentTypes = isLinkStatusCheck ? normalizeStringList(
7684
+ input.allowed_content_types ?? input.allowedContentTypes ?? input.expected_content_types ?? input.expectedContentTypes,
7685
+ `checks[${index}] allowed_content_types`
7686
+ ) ?? (expectedContentType ? [expectedContentType] : void 0) : void 0;
7681
7687
  if (isLinkStatusCheck) {
7682
7688
  if (minCount !== void 0 && (!Number.isInteger(minCount) || minCount < 0)) {
7683
7689
  throw new Error(`checks[${index}] ${type} min_count must be a non-negative integer.`);
@@ -7719,6 +7725,8 @@ function normalizeCheck(input, index) {
7719
7725
  same_origin_only: isLinkStatusCheck ? input.same_origin_only === true || input.sameOriginOnly === true : void 0,
7720
7726
  dedupe: isLinkStatusCheck ? input.dedupe === false ? false : true : void 0,
7721
7727
  require_nonzero_bytes: isLinkStatusCheck ? input.require_nonzero_bytes === true || input.requireNonzeroBytes === true : void 0,
7728
+ min_bytes: minBytes,
7729
+ allowed_content_types: allowedContentTypes,
7722
7730
  allow_get_fallback: isLinkStatusCheck ? input.allow_get_fallback === false || input.allowGetFallback === false ? false : true : void 0,
7723
7731
  max_overflow_px: numberValue(input.max_overflow_px),
7724
7732
  timeout_ms: numberValue(input.timeout_ms) ?? numberValue(input.timeoutMs),
@@ -7825,15 +7833,40 @@ function linkStatusIsAllowed(status, check) {
7825
7833
  const allowed = linkStatusAllowedStatuses(check);
7826
7834
  return allowed?.length ? allowed.includes(status) : status >= 200 && status < 400;
7827
7835
  }
7836
+ function linkStatusObservedBytes(result) {
7837
+ const bytes = numberValue(result.bytes);
7838
+ const contentLength = numberValue(result.content_length);
7839
+ return Math.max(bytes ?? 0, contentLength ?? 0) || void 0;
7840
+ }
7841
+ function normalizeLinkStatusContentType(value) {
7842
+ const normalized = value?.split(";")[0]?.trim().toLowerCase();
7843
+ return normalized || void 0;
7844
+ }
7845
+ function linkStatusContentTypeOk(result, check) {
7846
+ const expected = check.allowed_content_types;
7847
+ if (!expected?.length) return true;
7848
+ const actual = normalizeLinkStatusContentType(stringValue2(result.content_type));
7849
+ if (!actual) return false;
7850
+ return expected.some((contentType) => {
7851
+ const normalized = normalizeLinkStatusContentType(contentType);
7852
+ if (!normalized) return false;
7853
+ if (normalized.endsWith("/*")) return actual.startsWith(normalized.slice(0, -1));
7854
+ return actual === normalized;
7855
+ });
7856
+ }
7828
7857
  function linkStatusResultOk(result, check) {
7829
7858
  const status = numberValue(result.status);
7830
7859
  if (!linkStatusIsAllowed(status, check)) return false;
7831
7860
  if (stringValue2(result.error)) return false;
7832
7861
  if (result.ok === false) return false;
7862
+ if (!linkStatusContentTypeOk(result, check)) return false;
7833
7863
  if (check.require_nonzero_bytes) {
7834
- const bytes = numberValue(result.bytes);
7835
- const contentLength = numberValue(result.content_length);
7836
- return bytes !== void 0 && bytes > 0 || contentLength !== void 0 && contentLength > 0;
7864
+ const observedBytes = linkStatusObservedBytes(result);
7865
+ if (observedBytes === void 0 || observedBytes <= 0) return false;
7866
+ }
7867
+ if (check.min_bytes !== void 0) {
7868
+ const observedBytes = linkStatusObservedBytes(result);
7869
+ if (observedBytes === void 0 || observedBytes < check.min_bytes) return false;
7837
7870
  }
7838
7871
  return true;
7839
7872
  }
@@ -7862,7 +7895,10 @@ function summarizeLinkStatusEvidence(viewport, check) {
7862
7895
  status: numberValue(result.status) ?? null,
7863
7896
  method: stringValue2(result.method) ?? null,
7864
7897
  error: stringValue2(result.error) ?? null,
7865
- bytes: numberValue(result.bytes) ?? numberValue(result.content_length) ?? null
7898
+ content_type: stringValue2(result.content_type) ?? null,
7899
+ bytes: linkStatusObservedBytes(result) ?? null,
7900
+ min_bytes: check.min_bytes ?? null,
7901
+ allowed_content_types: check.allowed_content_types ?? null
7866
7902
  }));
7867
7903
  if (stringValue2(linkEvidence.error)) {
7868
7904
  failures.push({ code: "link_status_capture_failed", error: stringValue2(linkEvidence.error) ?? "" });
@@ -7899,6 +7935,8 @@ function summarizeLinkStatusEvidence(viewport, check) {
7899
7935
  failed_count: failures.length,
7900
7936
  truncated: linkEvidence.truncated === true,
7901
7937
  max_links: numberValue(linkEvidence.max_links) ?? check.max_links ?? 100,
7938
+ min_bytes: check.min_bytes ?? null,
7939
+ allowed_content_types: check.allowed_content_types ?? null,
7902
7940
  status_counts: statusCounts,
7903
7941
  failures: failures.slice(0, 20)
7904
7942
  };
@@ -8427,6 +8465,8 @@ function assessCheckFromEvidence(check, evidence) {
8427
8465
  min_count: check.min_count ?? null,
8428
8466
  allowed_statuses: linkStatusAllowedStatuses(check) || ["2xx", "3xx"],
8429
8467
  require_nonzero_bytes: check.require_nonzero_bytes === true,
8468
+ min_bytes: check.min_bytes ?? null,
8469
+ allowed_content_types: check.allowed_content_types ?? null,
8430
8470
  viewports: summaries.map((summary) => toJsonValue(summary)),
8431
8471
  failures: failed.flatMap((summary) => Array.isArray(summary.failures) ? summary.failures.map((failure) => toJsonValue({ viewport: stringValue2(summary.viewport) ?? null, failure })) : [])
8432
8472
  },
@@ -9014,15 +9054,41 @@ function linkStatusIsAllowed(status, check) {
9014
9054
  const allowed = linkStatusAllowedStatuses(check);
9015
9055
  return Array.isArray(allowed) && allowed.length ? allowed.includes(status) : status >= 200 && status < 400;
9016
9056
  }
9057
+ function linkStatusObservedBytes(result) {
9058
+ const bytes = typeof result.bytes === "number" && Number.isFinite(result.bytes) ? result.bytes : undefined;
9059
+ const contentLength = typeof result.content_length === "number" && Number.isFinite(result.content_length) ? result.content_length : undefined;
9060
+ const observed = Math.max(bytes || 0, contentLength || 0);
9061
+ return observed > 0 ? observed : undefined;
9062
+ }
9063
+ function normalizeLinkStatusContentType(value) {
9064
+ if (typeof value !== "string") return undefined;
9065
+ const normalized = (value.split(";")[0] || "").trim().toLowerCase();
9066
+ return normalized || undefined;
9067
+ }
9068
+ function linkStatusContentTypeOk(result, check) {
9069
+ if (!Array.isArray(check.allowed_content_types) || !check.allowed_content_types.length) return true;
9070
+ const actual = normalizeLinkStatusContentType(result.content_type);
9071
+ if (!actual) return false;
9072
+ return check.allowed_content_types.some((contentType) => {
9073
+ const normalized = normalizeLinkStatusContentType(contentType);
9074
+ if (!normalized) return false;
9075
+ if (normalized.endsWith("/*")) return actual.startsWith(normalized.slice(0, -1));
9076
+ return actual === normalized;
9077
+ });
9078
+ }
9017
9079
  function linkStatusResultOk(result, check) {
9018
9080
  if (!result || typeof result !== "object" || Array.isArray(result)) return false;
9019
9081
  if (!linkStatusIsAllowed(result.status, check)) return false;
9020
9082
  if (typeof result.error === "string" && result.error.trim()) return false;
9021
9083
  if (result.ok === false) return false;
9084
+ if (!linkStatusContentTypeOk(result, check)) return false;
9022
9085
  if (check.require_nonzero_bytes === true) {
9023
- const bytes = typeof result.bytes === "number" && Number.isFinite(result.bytes) ? result.bytes : undefined;
9024
- const contentLength = typeof result.content_length === "number" && Number.isFinite(result.content_length) ? result.content_length : undefined;
9025
- return (bytes !== undefined && bytes > 0) || (contentLength !== undefined && contentLength > 0);
9086
+ const observedBytes = linkStatusObservedBytes(result);
9087
+ if (observedBytes === undefined || observedBytes <= 0) return false;
9088
+ }
9089
+ if (typeof check.min_bytes === "number" && Number.isFinite(check.min_bytes)) {
9090
+ const observedBytes = linkStatusObservedBytes(result);
9091
+ if (observedBytes === undefined || observedBytes < check.min_bytes) return false;
9026
9092
  }
9027
9093
  return true;
9028
9094
  }
@@ -9050,11 +9116,10 @@ function summarizeLinkStatusEvidence(viewport, check) {
9050
9116
  status: typeof result.status === "number" && Number.isFinite(result.status) ? result.status : null,
9051
9117
  method: typeof result.method === "string" ? result.method : null,
9052
9118
  error: typeof result.error === "string" ? result.error : null,
9053
- bytes: typeof result.bytes === "number" && Number.isFinite(result.bytes)
9054
- ? result.bytes
9055
- : typeof result.content_length === "number" && Number.isFinite(result.content_length)
9056
- ? result.content_length
9057
- : null,
9119
+ content_type: typeof result.content_type === "string" ? result.content_type : null,
9120
+ bytes: linkStatusObservedBytes(result) ?? null,
9121
+ min_bytes: typeof check.min_bytes === "number" && Number.isFinite(check.min_bytes) ? check.min_bytes : null,
9122
+ allowed_content_types: Array.isArray(check.allowed_content_types) ? check.allowed_content_types : null,
9058
9123
  }));
9059
9124
  if (typeof linkEvidence.error === "string" && linkEvidence.error.trim()) {
9060
9125
  failures.push({ code: "link_status_capture_failed", error: linkEvidence.error });
@@ -9089,6 +9154,8 @@ function summarizeLinkStatusEvidence(viewport, check) {
9089
9154
  failed_count: failures.length,
9090
9155
  truncated: linkEvidence.truncated === true,
9091
9156
  max_links: typeof linkEvidence.max_links === "number" ? linkEvidence.max_links : check.max_links || 100,
9157
+ min_bytes: typeof check.min_bytes === "number" && Number.isFinite(check.min_bytes) ? check.min_bytes : null,
9158
+ allowed_content_types: Array.isArray(check.allowed_content_types) ? check.allowed_content_types : null,
9092
9159
  status_counts: linkEvidence.status_counts && typeof linkEvidence.status_counts === "object" && !Array.isArray(linkEvidence.status_counts) ? linkEvidence.status_counts : {},
9093
9160
  failures: failures.slice(0, 20),
9094
9161
  };
@@ -9785,6 +9852,8 @@ function assessProfile(profile, evidence) {
9785
9852
  min_count: check.min_count ?? null,
9786
9853
  allowed_statuses: linkStatusAllowedStatuses(check) || ["2xx", "3xx"],
9787
9854
  require_nonzero_bytes: check.require_nonzero_bytes === true,
9855
+ min_bytes: check.min_bytes ?? null,
9856
+ allowed_content_types: check.allowed_content_types ?? null,
9788
9857
  viewports: summaries,
9789
9858
  failures: failed.flatMap((summary) => Array.isArray(summary.failures)
9790
9859
  ? summary.failures.map((failure) => ({ viewport: summary.viewport || null, failure }))
@@ -10932,6 +11001,28 @@ function linkProbeAllowed(status, check) {
10932
11001
  : null;
10933
11002
  return allowed ? allowed.includes(status) : status >= 200 && status < 400;
10934
11003
  }
11004
+ function linkProbeObservedBytes(result) {
11005
+ const bytes = typeof result.bytes === "number" && Number.isFinite(result.bytes) ? result.bytes : undefined;
11006
+ const contentLength = typeof result.content_length === "number" && Number.isFinite(result.content_length) ? result.content_length : undefined;
11007
+ const observed = Math.max(bytes || 0, contentLength || 0);
11008
+ return observed > 0 ? observed : undefined;
11009
+ }
11010
+ function normalizeLinkProbeContentType(value) {
11011
+ if (typeof value !== "string") return undefined;
11012
+ const normalized = (value.split(";")[0] || "").trim().toLowerCase();
11013
+ return normalized || undefined;
11014
+ }
11015
+ function linkProbeContentTypeAllowed(result, check) {
11016
+ if (!Array.isArray(check.allowed_content_types) || !check.allowed_content_types.length) return true;
11017
+ const actual = normalizeLinkProbeContentType(result.content_type);
11018
+ if (!actual) return false;
11019
+ return check.allowed_content_types.some((contentType) => {
11020
+ const normalized = normalizeLinkProbeContentType(contentType);
11021
+ if (!normalized) return false;
11022
+ if (normalized.endsWith("/*")) return actual.startsWith(normalized.slice(0, -1));
11023
+ return actual === normalized;
11024
+ });
11025
+ }
10935
11026
  function linkProbeResponseFields(response, method) {
10936
11027
  const contentLengthHeader = response.headers && typeof response.headers.get === "function" ? response.headers.get("content-length") : null;
10937
11028
  const contentLength = contentLengthHeader && /^\d+$/.test(contentLengthHeader) ? Number(contentLengthHeader) : null;
@@ -10946,6 +11037,7 @@ function linkProbeResponseFields(response, method) {
10946
11037
  }
10947
11038
  async function probeLinkStatus(candidate, check) {
10948
11039
  const requireNonzeroBytes = check.require_nonzero_bytes === true;
11040
+ const minBytes = typeof check.min_bytes === "number" && Number.isFinite(check.min_bytes) ? Math.max(1, Math.floor(check.min_bytes)) : null;
10949
11041
  const allowGetFallback = check.allow_get_fallback !== false;
10950
11042
  const result = {
10951
11043
  url: candidate.url,
@@ -10963,7 +11055,7 @@ async function probeLinkStatus(candidate, check) {
10963
11055
  };
10964
11056
  const applyResponse = async (response, method, readBytes) => {
10965
11057
  Object.assign(result, linkProbeResponseFields(response, method));
10966
- if (readBytes || requireNonzeroBytes) {
11058
+ if (readBytes) {
10967
11059
  try {
10968
11060
  const buffer = await response.arrayBuffer();
10969
11061
  result.bytes = buffer.byteLength;
@@ -10972,7 +11064,9 @@ async function probeLinkStatus(candidate, check) {
10972
11064
  }
10973
11065
  }
10974
11066
  result.ok = linkProbeAllowed(result.status, check)
10975
- && (!requireNonzeroBytes || (typeof result.bytes === "number" ? result.bytes > 0 : typeof result.content_length === "number" && result.content_length > 0))
11067
+ && linkProbeContentTypeAllowed(result, check)
11068
+ && (!requireNonzeroBytes || ((linkProbeObservedBytes(result) || 0) > 0))
11069
+ && (minBytes === null || ((linkProbeObservedBytes(result) || 0) >= minBytes))
10976
11070
  && !result.error;
10977
11071
  };
10978
11072
  try {
@@ -10989,9 +11083,9 @@ async function probeLinkStatus(candidate, check) {
10989
11083
  method: "GET",
10990
11084
  redirect: "follow",
10991
11085
  cache: "no-store",
10992
- headers: { Range: "bytes=0-0" },
11086
+ headers: { Range: "bytes=0-" + String((minBytes || 1) - 1) },
10993
11087
  });
10994
- await applyResponse(response, "GET", requireNonzeroBytes);
11088
+ await applyResponse(response, "GET", requireNonzeroBytes || minBytes !== null);
10995
11089
  return result;
10996
11090
  } catch (error) {
10997
11091
  result.error = String(error && error.message ? error.message : error).slice(0, 500);
@@ -11045,7 +11139,10 @@ async function collectLinkStatus(check) {
11045
11139
  status: result.status,
11046
11140
  method: result.method,
11047
11141
  error: result.error,
11048
- bytes: result.bytes == null ? result.content_length : result.bytes,
11142
+ content_type: result.content_type,
11143
+ bytes: linkProbeObservedBytes(result) || null,
11144
+ min_bytes: typeof check.min_bytes === "number" && Number.isFinite(check.min_bytes) ? check.min_bytes : null,
11145
+ allowed_content_types: Array.isArray(check.allowed_content_types) ? check.allowed_content_types : null,
11049
11146
  }));
11050
11147
  const statusCounts = {};
11051
11148
  for (const result of results) {
@@ -11059,6 +11156,8 @@ async function collectLinkStatus(check) {
11059
11156
  same_origin_only: linkProbeSameOriginOnly(check),
11060
11157
  dedupe: linkProbeDedupe(check),
11061
11158
  require_nonzero_bytes: check.require_nonzero_bytes === true,
11159
+ min_bytes: typeof check.min_bytes === "number" && Number.isFinite(check.min_bytes) ? check.min_bytes : null,
11160
+ allowed_content_types: Array.isArray(check.allowed_content_types) ? check.allowed_content_types : null,
11062
11161
  allowed_statuses: Array.isArray(check.allowed_statuses) && check.allowed_statuses.length
11063
11162
  ? check.allowed_statuses
11064
11163
  : typeof check.expected_status === "number"
@@ -12279,9 +12378,13 @@ function profileCheckMarkdownTarget(check) {
12279
12378
  if (check.type === "link_status" || check.type === "artifact_link_status") {
12280
12379
  const expectedCount = cliFiniteNumber(evidence.expected_count);
12281
12380
  const minCount = cliFiniteNumber(evidence.min_count);
12282
- if (selector && expectedCount !== void 0) return `${markdownInlineCode(selector)} links = ${expectedCount}`;
12283
- if (selector && minCount !== void 0) return `${markdownInlineCode(selector)} links >= ${minCount}`;
12284
- return selector ? markdownInlineCode(selector) : void 0;
12381
+ const minBytes = cliFiniteNumber(evidence.min_bytes);
12382
+ const parts = [];
12383
+ if (expectedCount !== void 0) parts.push(`links = ${expectedCount}`);
12384
+ else if (minCount !== void 0) parts.push(`links >= ${minCount}`);
12385
+ if (minBytes !== void 0) parts.push(`bytes >= ${minBytes}`);
12386
+ if (selector && parts.length) return `${markdownInlineCode(selector)} ${parts.join(", ")}`;
12387
+ return selector ? markdownInlineCode(selector) : parts.join(", ") || void 0;
12285
12388
  }
12286
12389
  if (check.type === "no_horizontal_overflow" || check.type === "no_mobile_horizontal_overflow") {
12287
12390
  const maxOverflow = cliFiniteNumber(evidence.max_overflow_px);
@@ -12489,9 +12592,11 @@ function profileLinkStatusSummaryMarkdown(result) {
12489
12592
  const okTotal = viewports.reduce((sum, viewport) => sum + (cliFiniteNumber(viewport.ok_count) || 0), 0);
12490
12593
  const failedTotal = viewports.reduce((sum, viewport) => sum + (cliFiniteNumber(viewport.failed_count) || 0), 0);
12491
12594
  const truncatedTotal = viewports.filter((viewport) => viewport.truncated === true).length;
12595
+ const minBytes = cliFiniteNumber(evidence.min_bytes);
12596
+ const allowedContentTypes = Array.isArray(evidence.allowed_content_types) ? evidence.allowed_content_types.map(cliString).filter((value) => Boolean(value)) : [];
12492
12597
  const countText = totals.length ? totals.join("/") : "unknown";
12493
12598
  lines.push(
12494
- `- ${label}${selector ? ` ${markdownInlineCode(selector)}` : ""}: links ${countText}, ok ${okTotal}, failures ${failedTotal}${truncatedTotal ? `, truncated viewports ${truncatedTotal}` : ""}`
12599
+ `- ${label}${selector ? ` ${markdownInlineCode(selector)}` : ""}: links ${countText}, ok ${okTotal}, failures ${failedTotal}${truncatedTotal ? `, truncated viewports ${truncatedTotal}` : ""}${minBytes !== void 0 ? `, min bytes ${minBytes}` : ""}${allowedContentTypes.length ? `, content types ${allowedContentTypes.map((value) => markdownInlineCode(value)).join(", ")}` : ""}`
12495
12600
  );
12496
12601
  }
12497
12602
  return lines;
package/dist/cli.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  profileStatusExitCode,
11
11
  resolveRiddleProofProfileTargetUrl,
12
12
  resolveRiddleProofProfileTimeoutSec
13
- } from "./chunk-FQ4QBY2N.js";
13
+ } from "./chunk-5OCJKEHE.js";
14
14
  import {
15
15
  createRiddleApiClient,
16
16
  parseRiddleViewport
@@ -423,9 +423,13 @@ function profileCheckMarkdownTarget(check) {
423
423
  if (check.type === "link_status" || check.type === "artifact_link_status") {
424
424
  const expectedCount = cliFiniteNumber(evidence.expected_count);
425
425
  const minCount = cliFiniteNumber(evidence.min_count);
426
- if (selector && expectedCount !== void 0) return `${markdownInlineCode(selector)} links = ${expectedCount}`;
427
- if (selector && minCount !== void 0) return `${markdownInlineCode(selector)} links >= ${minCount}`;
428
- return selector ? markdownInlineCode(selector) : void 0;
426
+ const minBytes = cliFiniteNumber(evidence.min_bytes);
427
+ const parts = [];
428
+ if (expectedCount !== void 0) parts.push(`links = ${expectedCount}`);
429
+ else if (minCount !== void 0) parts.push(`links >= ${minCount}`);
430
+ if (minBytes !== void 0) parts.push(`bytes >= ${minBytes}`);
431
+ if (selector && parts.length) return `${markdownInlineCode(selector)} ${parts.join(", ")}`;
432
+ return selector ? markdownInlineCode(selector) : parts.join(", ") || void 0;
429
433
  }
430
434
  if (check.type === "no_horizontal_overflow" || check.type === "no_mobile_horizontal_overflow") {
431
435
  const maxOverflow = cliFiniteNumber(evidence.max_overflow_px);
@@ -633,9 +637,11 @@ function profileLinkStatusSummaryMarkdown(result) {
633
637
  const okTotal = viewports.reduce((sum, viewport) => sum + (cliFiniteNumber(viewport.ok_count) || 0), 0);
634
638
  const failedTotal = viewports.reduce((sum, viewport) => sum + (cliFiniteNumber(viewport.failed_count) || 0), 0);
635
639
  const truncatedTotal = viewports.filter((viewport) => viewport.truncated === true).length;
640
+ const minBytes = cliFiniteNumber(evidence.min_bytes);
641
+ const allowedContentTypes = Array.isArray(evidence.allowed_content_types) ? evidence.allowed_content_types.map(cliString).filter((value) => Boolean(value)) : [];
636
642
  const countText = totals.length ? totals.join("/") : "unknown";
637
643
  lines.push(
638
- `- ${label}${selector ? ` ${markdownInlineCode(selector)}` : ""}: links ${countText}, ok ${okTotal}, failures ${failedTotal}${truncatedTotal ? `, truncated viewports ${truncatedTotal}` : ""}`
644
+ `- ${label}${selector ? ` ${markdownInlineCode(selector)}` : ""}: links ${countText}, ok ${okTotal}, failures ${failedTotal}${truncatedTotal ? `, truncated viewports ${truncatedTotal}` : ""}${minBytes !== void 0 ? `, min bytes ${minBytes}` : ""}${allowedContentTypes.length ? `, content types ${allowedContentTypes.map((value) => markdownInlineCode(value)).join(", ")}` : ""}`
639
645
  );
640
646
  }
641
647
  return lines;
package/dist/index.cjs CHANGED
@@ -9514,6 +9514,12 @@ function normalizeCheck(input, index) {
9514
9514
  `checks[${index}] allowed_statuses`
9515
9515
  ) : void 0;
9516
9516
  const maxLinks = isLinkStatusCheck ? normalizePositiveInteger(input.max_links ?? input.maxLinks ?? input.limit, `checks[${index}] max_links`, 500) : void 0;
9517
+ const minBytes = isLinkStatusCheck ? normalizePositiveInteger(input.min_bytes ?? input.minBytes ?? input.min_response_bytes ?? input.minResponseBytes, `checks[${index}] min_bytes`) : void 0;
9518
+ const expectedContentType = isLinkStatusCheck ? stringValue5(input.expected_content_type) || stringValue5(input.expectedContentType) || stringValue5(input.content_type) || stringValue5(input.contentType) : void 0;
9519
+ const allowedContentTypes = isLinkStatusCheck ? normalizeStringList(
9520
+ input.allowed_content_types ?? input.allowedContentTypes ?? input.expected_content_types ?? input.expectedContentTypes,
9521
+ `checks[${index}] allowed_content_types`
9522
+ ) ?? (expectedContentType ? [expectedContentType] : void 0) : void 0;
9517
9523
  if (isLinkStatusCheck) {
9518
9524
  if (minCount !== void 0 && (!Number.isInteger(minCount) || minCount < 0)) {
9519
9525
  throw new Error(`checks[${index}] ${type} min_count must be a non-negative integer.`);
@@ -9555,6 +9561,8 @@ function normalizeCheck(input, index) {
9555
9561
  same_origin_only: isLinkStatusCheck ? input.same_origin_only === true || input.sameOriginOnly === true : void 0,
9556
9562
  dedupe: isLinkStatusCheck ? input.dedupe === false ? false : true : void 0,
9557
9563
  require_nonzero_bytes: isLinkStatusCheck ? input.require_nonzero_bytes === true || input.requireNonzeroBytes === true : void 0,
9564
+ min_bytes: minBytes,
9565
+ allowed_content_types: allowedContentTypes,
9558
9566
  allow_get_fallback: isLinkStatusCheck ? input.allow_get_fallback === false || input.allowGetFallback === false ? false : true : void 0,
9559
9567
  max_overflow_px: numberValue3(input.max_overflow_px),
9560
9568
  timeout_ms: numberValue3(input.timeout_ms) ?? numberValue3(input.timeoutMs),
@@ -9661,15 +9669,40 @@ function linkStatusIsAllowed(status, check) {
9661
9669
  const allowed = linkStatusAllowedStatuses(check);
9662
9670
  return allowed?.length ? allowed.includes(status) : status >= 200 && status < 400;
9663
9671
  }
9672
+ function linkStatusObservedBytes(result) {
9673
+ const bytes = numberValue3(result.bytes);
9674
+ const contentLength = numberValue3(result.content_length);
9675
+ return Math.max(bytes ?? 0, contentLength ?? 0) || void 0;
9676
+ }
9677
+ function normalizeLinkStatusContentType(value) {
9678
+ const normalized = value?.split(";")[0]?.trim().toLowerCase();
9679
+ return normalized || void 0;
9680
+ }
9681
+ function linkStatusContentTypeOk(result, check) {
9682
+ const expected = check.allowed_content_types;
9683
+ if (!expected?.length) return true;
9684
+ const actual = normalizeLinkStatusContentType(stringValue5(result.content_type));
9685
+ if (!actual) return false;
9686
+ return expected.some((contentType) => {
9687
+ const normalized = normalizeLinkStatusContentType(contentType);
9688
+ if (!normalized) return false;
9689
+ if (normalized.endsWith("/*")) return actual.startsWith(normalized.slice(0, -1));
9690
+ return actual === normalized;
9691
+ });
9692
+ }
9664
9693
  function linkStatusResultOk(result, check) {
9665
9694
  const status = numberValue3(result.status);
9666
9695
  if (!linkStatusIsAllowed(status, check)) return false;
9667
9696
  if (stringValue5(result.error)) return false;
9668
9697
  if (result.ok === false) return false;
9698
+ if (!linkStatusContentTypeOk(result, check)) return false;
9669
9699
  if (check.require_nonzero_bytes) {
9670
- const bytes = numberValue3(result.bytes);
9671
- const contentLength = numberValue3(result.content_length);
9672
- return bytes !== void 0 && bytes > 0 || contentLength !== void 0 && contentLength > 0;
9700
+ const observedBytes = linkStatusObservedBytes(result);
9701
+ if (observedBytes === void 0 || observedBytes <= 0) return false;
9702
+ }
9703
+ if (check.min_bytes !== void 0) {
9704
+ const observedBytes = linkStatusObservedBytes(result);
9705
+ if (observedBytes === void 0 || observedBytes < check.min_bytes) return false;
9673
9706
  }
9674
9707
  return true;
9675
9708
  }
@@ -9698,7 +9731,10 @@ function summarizeLinkStatusEvidence(viewport, check) {
9698
9731
  status: numberValue3(result.status) ?? null,
9699
9732
  method: stringValue5(result.method) ?? null,
9700
9733
  error: stringValue5(result.error) ?? null,
9701
- bytes: numberValue3(result.bytes) ?? numberValue3(result.content_length) ?? null
9734
+ content_type: stringValue5(result.content_type) ?? null,
9735
+ bytes: linkStatusObservedBytes(result) ?? null,
9736
+ min_bytes: check.min_bytes ?? null,
9737
+ allowed_content_types: check.allowed_content_types ?? null
9702
9738
  }));
9703
9739
  if (stringValue5(linkEvidence.error)) {
9704
9740
  failures.push({ code: "link_status_capture_failed", error: stringValue5(linkEvidence.error) ?? "" });
@@ -9735,6 +9771,8 @@ function summarizeLinkStatusEvidence(viewport, check) {
9735
9771
  failed_count: failures.length,
9736
9772
  truncated: linkEvidence.truncated === true,
9737
9773
  max_links: numberValue3(linkEvidence.max_links) ?? check.max_links ?? 100,
9774
+ min_bytes: check.min_bytes ?? null,
9775
+ allowed_content_types: check.allowed_content_types ?? null,
9738
9776
  status_counts: statusCounts,
9739
9777
  failures: failures.slice(0, 20)
9740
9778
  };
@@ -10263,6 +10301,8 @@ function assessCheckFromEvidence(check, evidence) {
10263
10301
  min_count: check.min_count ?? null,
10264
10302
  allowed_statuses: linkStatusAllowedStatuses(check) || ["2xx", "3xx"],
10265
10303
  require_nonzero_bytes: check.require_nonzero_bytes === true,
10304
+ min_bytes: check.min_bytes ?? null,
10305
+ allowed_content_types: check.allowed_content_types ?? null,
10266
10306
  viewports: summaries.map((summary) => toJsonValue(summary)),
10267
10307
  failures: failed.flatMap((summary) => Array.isArray(summary.failures) ? summary.failures.map((failure) => toJsonValue({ viewport: stringValue5(summary.viewport) ?? null, failure })) : [])
10268
10308
  },
@@ -10866,15 +10906,41 @@ function linkStatusIsAllowed(status, check) {
10866
10906
  const allowed = linkStatusAllowedStatuses(check);
10867
10907
  return Array.isArray(allowed) && allowed.length ? allowed.includes(status) : status >= 200 && status < 400;
10868
10908
  }
10909
+ function linkStatusObservedBytes(result) {
10910
+ const bytes = typeof result.bytes === "number" && Number.isFinite(result.bytes) ? result.bytes : undefined;
10911
+ const contentLength = typeof result.content_length === "number" && Number.isFinite(result.content_length) ? result.content_length : undefined;
10912
+ const observed = Math.max(bytes || 0, contentLength || 0);
10913
+ return observed > 0 ? observed : undefined;
10914
+ }
10915
+ function normalizeLinkStatusContentType(value) {
10916
+ if (typeof value !== "string") return undefined;
10917
+ const normalized = (value.split(";")[0] || "").trim().toLowerCase();
10918
+ return normalized || undefined;
10919
+ }
10920
+ function linkStatusContentTypeOk(result, check) {
10921
+ if (!Array.isArray(check.allowed_content_types) || !check.allowed_content_types.length) return true;
10922
+ const actual = normalizeLinkStatusContentType(result.content_type);
10923
+ if (!actual) return false;
10924
+ return check.allowed_content_types.some((contentType) => {
10925
+ const normalized = normalizeLinkStatusContentType(contentType);
10926
+ if (!normalized) return false;
10927
+ if (normalized.endsWith("/*")) return actual.startsWith(normalized.slice(0, -1));
10928
+ return actual === normalized;
10929
+ });
10930
+ }
10869
10931
  function linkStatusResultOk(result, check) {
10870
10932
  if (!result || typeof result !== "object" || Array.isArray(result)) return false;
10871
10933
  if (!linkStatusIsAllowed(result.status, check)) return false;
10872
10934
  if (typeof result.error === "string" && result.error.trim()) return false;
10873
10935
  if (result.ok === false) return false;
10936
+ if (!linkStatusContentTypeOk(result, check)) return false;
10874
10937
  if (check.require_nonzero_bytes === true) {
10875
- const bytes = typeof result.bytes === "number" && Number.isFinite(result.bytes) ? result.bytes : undefined;
10876
- const contentLength = typeof result.content_length === "number" && Number.isFinite(result.content_length) ? result.content_length : undefined;
10877
- return (bytes !== undefined && bytes > 0) || (contentLength !== undefined && contentLength > 0);
10938
+ const observedBytes = linkStatusObservedBytes(result);
10939
+ if (observedBytes === undefined || observedBytes <= 0) return false;
10940
+ }
10941
+ if (typeof check.min_bytes === "number" && Number.isFinite(check.min_bytes)) {
10942
+ const observedBytes = linkStatusObservedBytes(result);
10943
+ if (observedBytes === undefined || observedBytes < check.min_bytes) return false;
10878
10944
  }
10879
10945
  return true;
10880
10946
  }
@@ -10902,11 +10968,10 @@ function summarizeLinkStatusEvidence(viewport, check) {
10902
10968
  status: typeof result.status === "number" && Number.isFinite(result.status) ? result.status : null,
10903
10969
  method: typeof result.method === "string" ? result.method : null,
10904
10970
  error: typeof result.error === "string" ? result.error : null,
10905
- bytes: typeof result.bytes === "number" && Number.isFinite(result.bytes)
10906
- ? result.bytes
10907
- : typeof result.content_length === "number" && Number.isFinite(result.content_length)
10908
- ? result.content_length
10909
- : null,
10971
+ content_type: typeof result.content_type === "string" ? result.content_type : null,
10972
+ bytes: linkStatusObservedBytes(result) ?? null,
10973
+ min_bytes: typeof check.min_bytes === "number" && Number.isFinite(check.min_bytes) ? check.min_bytes : null,
10974
+ allowed_content_types: Array.isArray(check.allowed_content_types) ? check.allowed_content_types : null,
10910
10975
  }));
10911
10976
  if (typeof linkEvidence.error === "string" && linkEvidence.error.trim()) {
10912
10977
  failures.push({ code: "link_status_capture_failed", error: linkEvidence.error });
@@ -10941,6 +11006,8 @@ function summarizeLinkStatusEvidence(viewport, check) {
10941
11006
  failed_count: failures.length,
10942
11007
  truncated: linkEvidence.truncated === true,
10943
11008
  max_links: typeof linkEvidence.max_links === "number" ? linkEvidence.max_links : check.max_links || 100,
11009
+ min_bytes: typeof check.min_bytes === "number" && Number.isFinite(check.min_bytes) ? check.min_bytes : null,
11010
+ allowed_content_types: Array.isArray(check.allowed_content_types) ? check.allowed_content_types : null,
10944
11011
  status_counts: linkEvidence.status_counts && typeof linkEvidence.status_counts === "object" && !Array.isArray(linkEvidence.status_counts) ? linkEvidence.status_counts : {},
10945
11012
  failures: failures.slice(0, 20),
10946
11013
  };
@@ -11637,6 +11704,8 @@ function assessProfile(profile, evidence) {
11637
11704
  min_count: check.min_count ?? null,
11638
11705
  allowed_statuses: linkStatusAllowedStatuses(check) || ["2xx", "3xx"],
11639
11706
  require_nonzero_bytes: check.require_nonzero_bytes === true,
11707
+ min_bytes: check.min_bytes ?? null,
11708
+ allowed_content_types: check.allowed_content_types ?? null,
11640
11709
  viewports: summaries,
11641
11710
  failures: failed.flatMap((summary) => Array.isArray(summary.failures)
11642
11711
  ? summary.failures.map((failure) => ({ viewport: summary.viewport || null, failure }))
@@ -12784,6 +12853,28 @@ function linkProbeAllowed(status, check) {
12784
12853
  : null;
12785
12854
  return allowed ? allowed.includes(status) : status >= 200 && status < 400;
12786
12855
  }
12856
+ function linkProbeObservedBytes(result) {
12857
+ const bytes = typeof result.bytes === "number" && Number.isFinite(result.bytes) ? result.bytes : undefined;
12858
+ const contentLength = typeof result.content_length === "number" && Number.isFinite(result.content_length) ? result.content_length : undefined;
12859
+ const observed = Math.max(bytes || 0, contentLength || 0);
12860
+ return observed > 0 ? observed : undefined;
12861
+ }
12862
+ function normalizeLinkProbeContentType(value) {
12863
+ if (typeof value !== "string") return undefined;
12864
+ const normalized = (value.split(";")[0] || "").trim().toLowerCase();
12865
+ return normalized || undefined;
12866
+ }
12867
+ function linkProbeContentTypeAllowed(result, check) {
12868
+ if (!Array.isArray(check.allowed_content_types) || !check.allowed_content_types.length) return true;
12869
+ const actual = normalizeLinkProbeContentType(result.content_type);
12870
+ if (!actual) return false;
12871
+ return check.allowed_content_types.some((contentType) => {
12872
+ const normalized = normalizeLinkProbeContentType(contentType);
12873
+ if (!normalized) return false;
12874
+ if (normalized.endsWith("/*")) return actual.startsWith(normalized.slice(0, -1));
12875
+ return actual === normalized;
12876
+ });
12877
+ }
12787
12878
  function linkProbeResponseFields(response, method) {
12788
12879
  const contentLengthHeader = response.headers && typeof response.headers.get === "function" ? response.headers.get("content-length") : null;
12789
12880
  const contentLength = contentLengthHeader && /^\d+$/.test(contentLengthHeader) ? Number(contentLengthHeader) : null;
@@ -12798,6 +12889,7 @@ function linkProbeResponseFields(response, method) {
12798
12889
  }
12799
12890
  async function probeLinkStatus(candidate, check) {
12800
12891
  const requireNonzeroBytes = check.require_nonzero_bytes === true;
12892
+ const minBytes = typeof check.min_bytes === "number" && Number.isFinite(check.min_bytes) ? Math.max(1, Math.floor(check.min_bytes)) : null;
12801
12893
  const allowGetFallback = check.allow_get_fallback !== false;
12802
12894
  const result = {
12803
12895
  url: candidate.url,
@@ -12815,7 +12907,7 @@ async function probeLinkStatus(candidate, check) {
12815
12907
  };
12816
12908
  const applyResponse = async (response, method, readBytes) => {
12817
12909
  Object.assign(result, linkProbeResponseFields(response, method));
12818
- if (readBytes || requireNonzeroBytes) {
12910
+ if (readBytes) {
12819
12911
  try {
12820
12912
  const buffer = await response.arrayBuffer();
12821
12913
  result.bytes = buffer.byteLength;
@@ -12824,7 +12916,9 @@ async function probeLinkStatus(candidate, check) {
12824
12916
  }
12825
12917
  }
12826
12918
  result.ok = linkProbeAllowed(result.status, check)
12827
- && (!requireNonzeroBytes || (typeof result.bytes === "number" ? result.bytes > 0 : typeof result.content_length === "number" && result.content_length > 0))
12919
+ && linkProbeContentTypeAllowed(result, check)
12920
+ && (!requireNonzeroBytes || ((linkProbeObservedBytes(result) || 0) > 0))
12921
+ && (minBytes === null || ((linkProbeObservedBytes(result) || 0) >= minBytes))
12828
12922
  && !result.error;
12829
12923
  };
12830
12924
  try {
@@ -12841,9 +12935,9 @@ async function probeLinkStatus(candidate, check) {
12841
12935
  method: "GET",
12842
12936
  redirect: "follow",
12843
12937
  cache: "no-store",
12844
- headers: { Range: "bytes=0-0" },
12938
+ headers: { Range: "bytes=0-" + String((minBytes || 1) - 1) },
12845
12939
  });
12846
- await applyResponse(response, "GET", requireNonzeroBytes);
12940
+ await applyResponse(response, "GET", requireNonzeroBytes || minBytes !== null);
12847
12941
  return result;
12848
12942
  } catch (error) {
12849
12943
  result.error = String(error && error.message ? error.message : error).slice(0, 500);
@@ -12897,7 +12991,10 @@ async function collectLinkStatus(check) {
12897
12991
  status: result.status,
12898
12992
  method: result.method,
12899
12993
  error: result.error,
12900
- bytes: result.bytes == null ? result.content_length : result.bytes,
12994
+ content_type: result.content_type,
12995
+ bytes: linkProbeObservedBytes(result) || null,
12996
+ min_bytes: typeof check.min_bytes === "number" && Number.isFinite(check.min_bytes) ? check.min_bytes : null,
12997
+ allowed_content_types: Array.isArray(check.allowed_content_types) ? check.allowed_content_types : null,
12901
12998
  }));
12902
12999
  const statusCounts = {};
12903
13000
  for (const result of results) {
@@ -12911,6 +13008,8 @@ async function collectLinkStatus(check) {
12911
13008
  same_origin_only: linkProbeSameOriginOnly(check),
12912
13009
  dedupe: linkProbeDedupe(check),
12913
13010
  require_nonzero_bytes: check.require_nonzero_bytes === true,
13011
+ min_bytes: typeof check.min_bytes === "number" && Number.isFinite(check.min_bytes) ? check.min_bytes : null,
13012
+ allowed_content_types: Array.isArray(check.allowed_content_types) ? check.allowed_content_types : null,
12914
13013
  allowed_statuses: Array.isArray(check.allowed_statuses) && check.allowed_statuses.length
12915
13014
  ? check.allowed_statuses
12916
13015
  : typeof check.expected_status === "number"
package/dist/index.js CHANGED
@@ -58,7 +58,7 @@ import {
58
58
  resolveRiddleProofProfileTimeoutSec,
59
59
  slugifyRiddleProofProfileName,
60
60
  summarizeRiddleProofProfileResult
61
- } from "./chunk-FQ4QBY2N.js";
61
+ } from "./chunk-5OCJKEHE.js";
62
62
  import {
63
63
  DEFAULT_RIDDLE_API_BASE_URL,
64
64
  DEFAULT_RIDDLE_API_KEY_FILE,
package/dist/profile.cjs CHANGED
@@ -828,6 +828,12 @@ function normalizeCheck(input, index) {
828
828
  `checks[${index}] allowed_statuses`
829
829
  ) : void 0;
830
830
  const maxLinks = isLinkStatusCheck ? normalizePositiveInteger(input.max_links ?? input.maxLinks ?? input.limit, `checks[${index}] max_links`, 500) : void 0;
831
+ const minBytes = isLinkStatusCheck ? normalizePositiveInteger(input.min_bytes ?? input.minBytes ?? input.min_response_bytes ?? input.minResponseBytes, `checks[${index}] min_bytes`) : void 0;
832
+ const expectedContentType = isLinkStatusCheck ? stringValue(input.expected_content_type) || stringValue(input.expectedContentType) || stringValue(input.content_type) || stringValue(input.contentType) : void 0;
833
+ const allowedContentTypes = isLinkStatusCheck ? normalizeStringList(
834
+ input.allowed_content_types ?? input.allowedContentTypes ?? input.expected_content_types ?? input.expectedContentTypes,
835
+ `checks[${index}] allowed_content_types`
836
+ ) ?? (expectedContentType ? [expectedContentType] : void 0) : void 0;
831
837
  if (isLinkStatusCheck) {
832
838
  if (minCount !== void 0 && (!Number.isInteger(minCount) || minCount < 0)) {
833
839
  throw new Error(`checks[${index}] ${type} min_count must be a non-negative integer.`);
@@ -869,6 +875,8 @@ function normalizeCheck(input, index) {
869
875
  same_origin_only: isLinkStatusCheck ? input.same_origin_only === true || input.sameOriginOnly === true : void 0,
870
876
  dedupe: isLinkStatusCheck ? input.dedupe === false ? false : true : void 0,
871
877
  require_nonzero_bytes: isLinkStatusCheck ? input.require_nonzero_bytes === true || input.requireNonzeroBytes === true : void 0,
878
+ min_bytes: minBytes,
879
+ allowed_content_types: allowedContentTypes,
872
880
  allow_get_fallback: isLinkStatusCheck ? input.allow_get_fallback === false || input.allowGetFallback === false ? false : true : void 0,
873
881
  max_overflow_px: numberValue(input.max_overflow_px),
874
882
  timeout_ms: numberValue(input.timeout_ms) ?? numberValue(input.timeoutMs),
@@ -975,15 +983,40 @@ function linkStatusIsAllowed(status, check) {
975
983
  const allowed = linkStatusAllowedStatuses(check);
976
984
  return allowed?.length ? allowed.includes(status) : status >= 200 && status < 400;
977
985
  }
986
+ function linkStatusObservedBytes(result) {
987
+ const bytes = numberValue(result.bytes);
988
+ const contentLength = numberValue(result.content_length);
989
+ return Math.max(bytes ?? 0, contentLength ?? 0) || void 0;
990
+ }
991
+ function normalizeLinkStatusContentType(value) {
992
+ const normalized = value?.split(";")[0]?.trim().toLowerCase();
993
+ return normalized || void 0;
994
+ }
995
+ function linkStatusContentTypeOk(result, check) {
996
+ const expected = check.allowed_content_types;
997
+ if (!expected?.length) return true;
998
+ const actual = normalizeLinkStatusContentType(stringValue(result.content_type));
999
+ if (!actual) return false;
1000
+ return expected.some((contentType) => {
1001
+ const normalized = normalizeLinkStatusContentType(contentType);
1002
+ if (!normalized) return false;
1003
+ if (normalized.endsWith("/*")) return actual.startsWith(normalized.slice(0, -1));
1004
+ return actual === normalized;
1005
+ });
1006
+ }
978
1007
  function linkStatusResultOk(result, check) {
979
1008
  const status = numberValue(result.status);
980
1009
  if (!linkStatusIsAllowed(status, check)) return false;
981
1010
  if (stringValue(result.error)) return false;
982
1011
  if (result.ok === false) return false;
1012
+ if (!linkStatusContentTypeOk(result, check)) return false;
983
1013
  if (check.require_nonzero_bytes) {
984
- const bytes = numberValue(result.bytes);
985
- const contentLength = numberValue(result.content_length);
986
- return bytes !== void 0 && bytes > 0 || contentLength !== void 0 && contentLength > 0;
1014
+ const observedBytes = linkStatusObservedBytes(result);
1015
+ if (observedBytes === void 0 || observedBytes <= 0) return false;
1016
+ }
1017
+ if (check.min_bytes !== void 0) {
1018
+ const observedBytes = linkStatusObservedBytes(result);
1019
+ if (observedBytes === void 0 || observedBytes < check.min_bytes) return false;
987
1020
  }
988
1021
  return true;
989
1022
  }
@@ -1012,7 +1045,10 @@ function summarizeLinkStatusEvidence(viewport, check) {
1012
1045
  status: numberValue(result.status) ?? null,
1013
1046
  method: stringValue(result.method) ?? null,
1014
1047
  error: stringValue(result.error) ?? null,
1015
- bytes: numberValue(result.bytes) ?? numberValue(result.content_length) ?? null
1048
+ content_type: stringValue(result.content_type) ?? null,
1049
+ bytes: linkStatusObservedBytes(result) ?? null,
1050
+ min_bytes: check.min_bytes ?? null,
1051
+ allowed_content_types: check.allowed_content_types ?? null
1016
1052
  }));
1017
1053
  if (stringValue(linkEvidence.error)) {
1018
1054
  failures.push({ code: "link_status_capture_failed", error: stringValue(linkEvidence.error) ?? "" });
@@ -1049,6 +1085,8 @@ function summarizeLinkStatusEvidence(viewport, check) {
1049
1085
  failed_count: failures.length,
1050
1086
  truncated: linkEvidence.truncated === true,
1051
1087
  max_links: numberValue(linkEvidence.max_links) ?? check.max_links ?? 100,
1088
+ min_bytes: check.min_bytes ?? null,
1089
+ allowed_content_types: check.allowed_content_types ?? null,
1052
1090
  status_counts: statusCounts,
1053
1091
  failures: failures.slice(0, 20)
1054
1092
  };
@@ -1577,6 +1615,8 @@ function assessCheckFromEvidence(check, evidence) {
1577
1615
  min_count: check.min_count ?? null,
1578
1616
  allowed_statuses: linkStatusAllowedStatuses(check) || ["2xx", "3xx"],
1579
1617
  require_nonzero_bytes: check.require_nonzero_bytes === true,
1618
+ min_bytes: check.min_bytes ?? null,
1619
+ allowed_content_types: check.allowed_content_types ?? null,
1580
1620
  viewports: summaries.map((summary) => toJsonValue(summary)),
1581
1621
  failures: failed.flatMap((summary) => Array.isArray(summary.failures) ? summary.failures.map((failure) => toJsonValue({ viewport: stringValue(summary.viewport) ?? null, failure })) : [])
1582
1622
  },
@@ -2180,15 +2220,41 @@ function linkStatusIsAllowed(status, check) {
2180
2220
  const allowed = linkStatusAllowedStatuses(check);
2181
2221
  return Array.isArray(allowed) && allowed.length ? allowed.includes(status) : status >= 200 && status < 400;
2182
2222
  }
2223
+ function linkStatusObservedBytes(result) {
2224
+ const bytes = typeof result.bytes === "number" && Number.isFinite(result.bytes) ? result.bytes : undefined;
2225
+ const contentLength = typeof result.content_length === "number" && Number.isFinite(result.content_length) ? result.content_length : undefined;
2226
+ const observed = Math.max(bytes || 0, contentLength || 0);
2227
+ return observed > 0 ? observed : undefined;
2228
+ }
2229
+ function normalizeLinkStatusContentType(value) {
2230
+ if (typeof value !== "string") return undefined;
2231
+ const normalized = (value.split(";")[0] || "").trim().toLowerCase();
2232
+ return normalized || undefined;
2233
+ }
2234
+ function linkStatusContentTypeOk(result, check) {
2235
+ if (!Array.isArray(check.allowed_content_types) || !check.allowed_content_types.length) return true;
2236
+ const actual = normalizeLinkStatusContentType(result.content_type);
2237
+ if (!actual) return false;
2238
+ return check.allowed_content_types.some((contentType) => {
2239
+ const normalized = normalizeLinkStatusContentType(contentType);
2240
+ if (!normalized) return false;
2241
+ if (normalized.endsWith("/*")) return actual.startsWith(normalized.slice(0, -1));
2242
+ return actual === normalized;
2243
+ });
2244
+ }
2183
2245
  function linkStatusResultOk(result, check) {
2184
2246
  if (!result || typeof result !== "object" || Array.isArray(result)) return false;
2185
2247
  if (!linkStatusIsAllowed(result.status, check)) return false;
2186
2248
  if (typeof result.error === "string" && result.error.trim()) return false;
2187
2249
  if (result.ok === false) return false;
2250
+ if (!linkStatusContentTypeOk(result, check)) return false;
2188
2251
  if (check.require_nonzero_bytes === true) {
2189
- const bytes = typeof result.bytes === "number" && Number.isFinite(result.bytes) ? result.bytes : undefined;
2190
- const contentLength = typeof result.content_length === "number" && Number.isFinite(result.content_length) ? result.content_length : undefined;
2191
- return (bytes !== undefined && bytes > 0) || (contentLength !== undefined && contentLength > 0);
2252
+ const observedBytes = linkStatusObservedBytes(result);
2253
+ if (observedBytes === undefined || observedBytes <= 0) return false;
2254
+ }
2255
+ if (typeof check.min_bytes === "number" && Number.isFinite(check.min_bytes)) {
2256
+ const observedBytes = linkStatusObservedBytes(result);
2257
+ if (observedBytes === undefined || observedBytes < check.min_bytes) return false;
2192
2258
  }
2193
2259
  return true;
2194
2260
  }
@@ -2216,11 +2282,10 @@ function summarizeLinkStatusEvidence(viewport, check) {
2216
2282
  status: typeof result.status === "number" && Number.isFinite(result.status) ? result.status : null,
2217
2283
  method: typeof result.method === "string" ? result.method : null,
2218
2284
  error: typeof result.error === "string" ? result.error : null,
2219
- bytes: typeof result.bytes === "number" && Number.isFinite(result.bytes)
2220
- ? result.bytes
2221
- : typeof result.content_length === "number" && Number.isFinite(result.content_length)
2222
- ? result.content_length
2223
- : null,
2285
+ content_type: typeof result.content_type === "string" ? result.content_type : null,
2286
+ bytes: linkStatusObservedBytes(result) ?? null,
2287
+ min_bytes: typeof check.min_bytes === "number" && Number.isFinite(check.min_bytes) ? check.min_bytes : null,
2288
+ allowed_content_types: Array.isArray(check.allowed_content_types) ? check.allowed_content_types : null,
2224
2289
  }));
2225
2290
  if (typeof linkEvidence.error === "string" && linkEvidence.error.trim()) {
2226
2291
  failures.push({ code: "link_status_capture_failed", error: linkEvidence.error });
@@ -2255,6 +2320,8 @@ function summarizeLinkStatusEvidence(viewport, check) {
2255
2320
  failed_count: failures.length,
2256
2321
  truncated: linkEvidence.truncated === true,
2257
2322
  max_links: typeof linkEvidence.max_links === "number" ? linkEvidence.max_links : check.max_links || 100,
2323
+ min_bytes: typeof check.min_bytes === "number" && Number.isFinite(check.min_bytes) ? check.min_bytes : null,
2324
+ allowed_content_types: Array.isArray(check.allowed_content_types) ? check.allowed_content_types : null,
2258
2325
  status_counts: linkEvidence.status_counts && typeof linkEvidence.status_counts === "object" && !Array.isArray(linkEvidence.status_counts) ? linkEvidence.status_counts : {},
2259
2326
  failures: failures.slice(0, 20),
2260
2327
  };
@@ -2951,6 +3018,8 @@ function assessProfile(profile, evidence) {
2951
3018
  min_count: check.min_count ?? null,
2952
3019
  allowed_statuses: linkStatusAllowedStatuses(check) || ["2xx", "3xx"],
2953
3020
  require_nonzero_bytes: check.require_nonzero_bytes === true,
3021
+ min_bytes: check.min_bytes ?? null,
3022
+ allowed_content_types: check.allowed_content_types ?? null,
2954
3023
  viewports: summaries,
2955
3024
  failures: failed.flatMap((summary) => Array.isArray(summary.failures)
2956
3025
  ? summary.failures.map((failure) => ({ viewport: summary.viewport || null, failure }))
@@ -4098,6 +4167,28 @@ function linkProbeAllowed(status, check) {
4098
4167
  : null;
4099
4168
  return allowed ? allowed.includes(status) : status >= 200 && status < 400;
4100
4169
  }
4170
+ function linkProbeObservedBytes(result) {
4171
+ const bytes = typeof result.bytes === "number" && Number.isFinite(result.bytes) ? result.bytes : undefined;
4172
+ const contentLength = typeof result.content_length === "number" && Number.isFinite(result.content_length) ? result.content_length : undefined;
4173
+ const observed = Math.max(bytes || 0, contentLength || 0);
4174
+ return observed > 0 ? observed : undefined;
4175
+ }
4176
+ function normalizeLinkProbeContentType(value) {
4177
+ if (typeof value !== "string") return undefined;
4178
+ const normalized = (value.split(";")[0] || "").trim().toLowerCase();
4179
+ return normalized || undefined;
4180
+ }
4181
+ function linkProbeContentTypeAllowed(result, check) {
4182
+ if (!Array.isArray(check.allowed_content_types) || !check.allowed_content_types.length) return true;
4183
+ const actual = normalizeLinkProbeContentType(result.content_type);
4184
+ if (!actual) return false;
4185
+ return check.allowed_content_types.some((contentType) => {
4186
+ const normalized = normalizeLinkProbeContentType(contentType);
4187
+ if (!normalized) return false;
4188
+ if (normalized.endsWith("/*")) return actual.startsWith(normalized.slice(0, -1));
4189
+ return actual === normalized;
4190
+ });
4191
+ }
4101
4192
  function linkProbeResponseFields(response, method) {
4102
4193
  const contentLengthHeader = response.headers && typeof response.headers.get === "function" ? response.headers.get("content-length") : null;
4103
4194
  const contentLength = contentLengthHeader && /^\d+$/.test(contentLengthHeader) ? Number(contentLengthHeader) : null;
@@ -4112,6 +4203,7 @@ function linkProbeResponseFields(response, method) {
4112
4203
  }
4113
4204
  async function probeLinkStatus(candidate, check) {
4114
4205
  const requireNonzeroBytes = check.require_nonzero_bytes === true;
4206
+ const minBytes = typeof check.min_bytes === "number" && Number.isFinite(check.min_bytes) ? Math.max(1, Math.floor(check.min_bytes)) : null;
4115
4207
  const allowGetFallback = check.allow_get_fallback !== false;
4116
4208
  const result = {
4117
4209
  url: candidate.url,
@@ -4129,7 +4221,7 @@ async function probeLinkStatus(candidate, check) {
4129
4221
  };
4130
4222
  const applyResponse = async (response, method, readBytes) => {
4131
4223
  Object.assign(result, linkProbeResponseFields(response, method));
4132
- if (readBytes || requireNonzeroBytes) {
4224
+ if (readBytes) {
4133
4225
  try {
4134
4226
  const buffer = await response.arrayBuffer();
4135
4227
  result.bytes = buffer.byteLength;
@@ -4138,7 +4230,9 @@ async function probeLinkStatus(candidate, check) {
4138
4230
  }
4139
4231
  }
4140
4232
  result.ok = linkProbeAllowed(result.status, check)
4141
- && (!requireNonzeroBytes || (typeof result.bytes === "number" ? result.bytes > 0 : typeof result.content_length === "number" && result.content_length > 0))
4233
+ && linkProbeContentTypeAllowed(result, check)
4234
+ && (!requireNonzeroBytes || ((linkProbeObservedBytes(result) || 0) > 0))
4235
+ && (minBytes === null || ((linkProbeObservedBytes(result) || 0) >= minBytes))
4142
4236
  && !result.error;
4143
4237
  };
4144
4238
  try {
@@ -4155,9 +4249,9 @@ async function probeLinkStatus(candidate, check) {
4155
4249
  method: "GET",
4156
4250
  redirect: "follow",
4157
4251
  cache: "no-store",
4158
- headers: { Range: "bytes=0-0" },
4252
+ headers: { Range: "bytes=0-" + String((minBytes || 1) - 1) },
4159
4253
  });
4160
- await applyResponse(response, "GET", requireNonzeroBytes);
4254
+ await applyResponse(response, "GET", requireNonzeroBytes || minBytes !== null);
4161
4255
  return result;
4162
4256
  } catch (error) {
4163
4257
  result.error = String(error && error.message ? error.message : error).slice(0, 500);
@@ -4211,7 +4305,10 @@ async function collectLinkStatus(check) {
4211
4305
  status: result.status,
4212
4306
  method: result.method,
4213
4307
  error: result.error,
4214
- bytes: result.bytes == null ? result.content_length : result.bytes,
4308
+ content_type: result.content_type,
4309
+ bytes: linkProbeObservedBytes(result) || null,
4310
+ min_bytes: typeof check.min_bytes === "number" && Number.isFinite(check.min_bytes) ? check.min_bytes : null,
4311
+ allowed_content_types: Array.isArray(check.allowed_content_types) ? check.allowed_content_types : null,
4215
4312
  }));
4216
4313
  const statusCounts = {};
4217
4314
  for (const result of results) {
@@ -4225,6 +4322,8 @@ async function collectLinkStatus(check) {
4225
4322
  same_origin_only: linkProbeSameOriginOnly(check),
4226
4323
  dedupe: linkProbeDedupe(check),
4227
4324
  require_nonzero_bytes: check.require_nonzero_bytes === true,
4325
+ min_bytes: typeof check.min_bytes === "number" && Number.isFinite(check.min_bytes) ? check.min_bytes : null,
4326
+ allowed_content_types: Array.isArray(check.allowed_content_types) ? check.allowed_content_types : null,
4228
4327
  allowed_statuses: Array.isArray(check.allowed_statuses) && check.allowed_statuses.length
4229
4328
  ? check.allowed_statuses
4230
4329
  : typeof check.expected_status === "number"
@@ -138,6 +138,8 @@ interface RiddleProofProfileCheck {
138
138
  same_origin_only?: boolean;
139
139
  dedupe?: boolean;
140
140
  require_nonzero_bytes?: boolean;
141
+ min_bytes?: number;
142
+ allowed_content_types?: string[];
141
143
  allow_get_fallback?: boolean;
142
144
  max_overflow_px?: number;
143
145
  timeout_ms?: number;
package/dist/profile.d.ts CHANGED
@@ -138,6 +138,8 @@ interface RiddleProofProfileCheck {
138
138
  same_origin_only?: boolean;
139
139
  dedupe?: boolean;
140
140
  require_nonzero_bytes?: boolean;
141
+ min_bytes?: number;
142
+ allowed_content_types?: string[];
141
143
  allow_get_fallback?: boolean;
142
144
  max_overflow_px?: number;
143
145
  timeout_ms?: number;
package/dist/profile.js CHANGED
@@ -19,7 +19,7 @@ import {
19
19
  resolveRiddleProofProfileTimeoutSec,
20
20
  slugifyRiddleProofProfileName,
21
21
  summarizeRiddleProofProfileResult
22
- } from "./chunk-FQ4QBY2N.js";
22
+ } from "./chunk-5OCJKEHE.js";
23
23
  export {
24
24
  RIDDLE_PROOF_PROFILE_CHECK_TYPES,
25
25
  RIDDLE_PROOF_PROFILE_EVIDENCE_VERSION,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@riddledc/riddle-proof",
3
- "version": "0.7.94",
3
+ "version": "0.7.95",
4
4
  "description": "Reusable Riddle Proof contracts and helpers for evidence-backed agent changes.",
5
5
  "license": "MIT",
6
6
  "author": "RiddleDC",