fix: bound social preview smoke response

This commit is contained in:
2026-09-05 01:06:01 +03:00
parent c958d4a05f
commit f58b079c18
2 changed files with 102 additions and 4 deletions

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env node
import { pathToFileURL } from 'node:url';
import { PERFORMANCE_BUDGETS } from './lib/bundle-budgets.mjs';
const sha256Pattern = /^[a-f0-9]{64}$/u;
const privateIdentifiers = /(?:simpastil|simpasteel|neon[ -]?debugger|yourhealth)/iu;
@@ -144,6 +145,56 @@ async function request(fetchImpl, url, init = {}) {
return response;
}
async function cancelResponseBody(response) {
try { await response.body?.cancel(); } catch { /* Preserve the validation error that required cancellation. */ }
}
async function countBoundedResponseBody(response, label, maxBytes) {
const contentLengthHeader = response.headers.get('content-length');
let declaredBytes = null;
if (contentLengthHeader !== null) {
if (!/^(?:0|[1-9][0-9]*)$/u.test(contentLengthHeader)) {
await cancelResponseBody(response);
throw new Error(`${label} content-length is malformed.`);
}
declaredBytes = Number(contentLengthHeader);
if (!Number.isSafeInteger(declaredBytes)) {
await cancelResponseBody(response);
throw new Error(`${label} content-length is outside the safe integer range.`);
}
if (declaredBytes === 0) {
await cancelResponseBody(response);
throw new Error(`${label} response is empty.`);
}
if (declaredBytes > maxBytes) {
await cancelResponseBody(response);
throw new Error(`${label} exceeds the ${maxBytes}-byte project-still limit.`);
}
}
if (response.body === null) throw new Error(`${label} response body is missing.`);
const reader = response.body.getReader();
let receivedBytes = 0;
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
receivedBytes += value.byteLength;
if (receivedBytes > maxBytes) {
try { await reader.cancel(); } catch { /* Preserve the byte-limit validation error. */ }
throw new Error(`${label} exceeds the ${maxBytes}-byte project-still limit.`);
}
}
} finally {
reader.releaseLock();
}
if (receivedBytes === 0) throw new Error(`${label} response is empty.`);
if (declaredBytes !== null && receivedBytes !== declaredBytes) {
throw new Error(`${label} content-length does not match the streamed byte count.`);
}
return receivedBytes;
}
export async function verifyProductionSmoke(options) {
const canonical = rootHttpsUrl(options.canonicalUrl, 'Canonical URL');
if (!sha256Pattern.test(options.expectedManifestDigest)) throw new Error('Expected signed manifest digest must be lowercase SHA-256.');
@@ -182,7 +233,11 @@ export async function verifyProductionSmoke(options) {
if (socialImageType === null || !new RegExp(`^${publicMetadata.socialImageType}(?:;|$)`, 'u').test(socialImageType)) {
throw new Error('Production social image content-type is missing or unsupported.');
}
if ((await socialImageResponse.arrayBuffer()).byteLength === 0) throw new Error('Production social image response is empty.');
await countBoundedResponseBody(
socialImageResponse,
'Production social image',
PERFORMANCE_BUDGETS.projectStill,
);
for (const path of ['/robots.txt', '/sitemap.xml']) {
const response = await request(fetchImpl, new URL(path, canonical));
if (response.status !== 200) throw new Error(`${path} did not return 200.`);