chore: merge latest main for CI verification

This commit is contained in:
gensokyo-dev
2026-10-19 14:19:22 +00:00
16 changed files with 662 additions and 72 deletions
+6
View File
@@ -776,6 +776,12 @@ after SDK conversion and check advertised validation and actual calls.
Individual model APIs can impose additional nesting limits; complete producer
captures can exceed ten structural levels.
`search_bytes` advertises its pattern and mask as descriptive strings. Its
shared canonical validator scans byte pairs, whitespace, wildcards and aligned
masks without a repeating regular expression or temporary byte arrays. The
server rejects malformed or unconstrained patterns before provider dispatch;
the advertised strings alone do not express those rules.
Boolean literal constraints in input schemas are relaxed only in this
advertisement. For example, a boolean `const` is presented as `type: boolean`;
an affected `not` is omitted, conditional predicates and their branches are
+5 -2
View File
@@ -1,10 +1,11 @@
import { inferJsonShape, type JsonShape } from "../domain/jsonShape.js";
import { decodePaddedBase64 } from "../domain/paddedBase64.js";
import {
recordValue,
cdpStringValue,
type UnknownRecord,
} from "./CdpCaptureValues.js";
import { decodeBase64, isJsonContentType } from "./CdpCaptureEventHelpers.js";
import { isJsonContentType } from "./CdpCaptureEventHelpers.js";
import type { CdpCaptureEventsState } from "./CdpCaptureEventState.js";
import type { NetworkState } from "./CdpCaptureEventTypes.js";
@@ -20,7 +21,9 @@ export const ingestResponseBodyShape = (
invalidResponseBodyShape(state, requestId);
return;
}
const decoded = base64Encoded ? decodeBase64(body)?.toString("utf8") : body;
const decoded = base64Encoded
? decodePaddedBase64(body)?.toString("utf8")
: body;
if (decoded === undefined) {
invalidResponseBodyShape(state, requestId);
return;
+5 -4
View File
@@ -1,6 +1,7 @@
import type { WebPageInspection } from "../domain/browserObservationSchemas.js";
import { sanitizeBrowserUrl } from "../domain/browserObservation.js";
import { inferJsonShape } from "../domain/jsonShape.js";
import { paddedBase64ByteLength } from "../domain/paddedBase64.js";
import { safeResponseMetadata } from "./CdpSafeMetadata.js";
import {
allowedSanitizedUrl,
@@ -18,7 +19,6 @@ import {
} from "./CdpCaptureEventBodyShapes.js";
import {
consolePrimitive,
decodeBase64,
exclusionReasonForUrl,
executionContextKey,
firstCallFrame,
@@ -508,14 +508,15 @@ export const handleWebSocketFrame = (
const response = recordValue(params.response);
const payload = cdpStringValue(response?.payloadData) ?? "";
const opcode = Math.max(0, Math.trunc(numberValue(response?.opcode) ?? 0));
const decoded = opcode === 1 ? undefined : decodeBase64(payload);
if (opcode !== 1 && decoded === undefined)
const decodedBytes =
opcode === 1 ? undefined : paddedBase64ByteLength(payload);
if (opcode !== 1 && decodedBytes === undefined)
state.completeness.exclude("websocket_frames", "invalid_protocol_value");
connection.events.push({
direction,
opcode,
payload_bytes:
opcode === 1 ? Buffer.byteLength(payload) : (decoded?.byteLength ?? null),
opcode === 1 ? Buffer.byteLength(payload) : (decodedBytes ?? null),
payload_shape: captureWebSocketShape(state, payload, opcode),
});
};
-7
View File
@@ -111,10 +111,3 @@ export const consolePrimitive = (
return undefined;
}
};
const BASE64 = /^(?:[A-Za-z\d+/]{4})*(?:[A-Za-z\d+/]{2}==|[A-Za-z\d+/]{3}=)?$/u;
export const decodeBase64 = (value: string): Buffer | undefined =>
value.length % 4 === 0 && BASE64.test(value)
? Buffer.from(value, "base64")
: undefined;
+4 -14
View File
@@ -4,6 +4,7 @@ import canonicalize from "canonicalize";
import type { InspectWebPageInput } from "../domain/browserObservation.js";
import type { WebPageInspection } from "../domain/browserObservationSchemas.js";
import { decodePaddedBase64 } from "../domain/paddedBase64.js";
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
import { CdpConnection } from "./CdpConnection.js";
import { optionalCdpCommand } from "./CdpOptionalCommand.js";
@@ -413,13 +414,13 @@ const addCacheEntry = async (
run.limitations,
);
const body = cdpStringValue(recordValue(recordValue(raw)?.response)?.body);
const decoded = body === undefined ? null : decodeBase64(body);
const complete = decoded !== null;
const decoded = body === undefined ? undefined : decodePaddedBase64(body);
const complete = decoded !== undefined;
if (!complete) run.state.complete = false;
addFingerprint(run.state, {
scope: "cache_entry",
identity: { cache: cache.name, requestURL, requestMethod, requestHeaders },
value: decoded ?? undefined,
value: decoded,
complete,
});
};
@@ -541,14 +542,3 @@ const remoteObjectComplete = (value: unknown): boolean => {
return false;
}
};
const decodeBase64 = (value: string): Buffer | null => {
if (
value.length % 4 !== 0 ||
!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u.test(
value,
)
)
return null;
return Buffer.from(value, "base64");
};
+184
View File
@@ -0,0 +1,184 @@
import { createHash } from "node:crypto";
import { expect, it, vi } from "vitest";
import { inspectWebPageInputSchema } from "../domain/browserObservation.js";
import { CdpCaptureEvents } from "./CdpCaptureEvents.js";
import { captureStorageFingerprints } from "./CdpCaptureStorageFingerprints.js";
import type { CdpConnection } from "./CdpConnection.js";
const ORIGIN = "https://example.test";
const CDP_ENDPOINT = "http://127.0.0.1:9222";
const BODY_BYTES = 4 * 1024 * 1024;
const captureCacheFingerprint = async (encodedBody: string) => {
const send = vi.fn(async (method: string) => {
if (method === "Network.getCookies") return { cookies: [] };
if (method === "CacheStorage.requestEntries")
return {
cacheDataEntries: [
{
requestURL: `${ORIGIN}/cached`,
requestMethod: "GET",
requestHeaders: [],
},
],
returnCount: 1,
};
if (method === "CacheStorage.requestCachedResponse")
return { response: { body: encodedBody } };
throw new Error(`Unexpected CDP command ${method}`);
});
const input = inspectWebPageInputSchema.parse({
cdp_endpoint: CDP_ENDPOINT,
allowed_origins: [ORIGIN],
target_id: "page",
});
return captureStorageFingerprints({
context: {
connection: { send } as unknown as CdpConnection,
sessionId: "page-session",
input,
},
origin: ORIGIN,
cookieUrl: `${ORIGIN}/`,
local: { items: [], complete: true },
session: { items: [], complete: true },
indexedDbNames: [],
caches: [{ name: "cache", id: "cache-id" }],
limitations: [],
});
};
const createEvents = (includeJsonBodyShapes = true): CdpCaptureEvents =>
new CdpCaptureEvents(
inspectWebPageInputSchema.parse({
cdp_endpoint: CDP_ENDPOINT,
allowed_origins: [ORIGIN],
target_id: "page",
include_json_body_shapes: includeJsonBodyShapes,
}),
new Set([ORIGIN]),
);
const startJsonResponse = (
events: CdpCaptureEvents,
requestId: string,
path: string,
): void => {
events.ingest({
method: "Network.requestWillBeSent",
params: {
requestId,
request: { url: `${ORIGIN}${path}`, method: "GET" },
initiator: { type: "other" },
timestamp: 1,
},
});
events.ingest({
method: "Network.responseReceived",
params: {
requestId,
response: {
url: `${ORIGIN}${path}`,
status: 200,
mimeType: "application/json",
},
},
});
};
it("fingerprints large cached bodies and retains CDP padding-bit acceptance", async () => {
const body = Buffer.alloc(BODY_BYTES, 0x81);
const large = await captureCacheFingerprint(body.toString("base64"));
expect(large.complete).toBe(true);
expect(large.items[0]?.value_sha256).toBe(
createHash("sha256").update(body).digest("hex"),
);
const padBits = await captureCacheFingerprint("e31=");
expect(padBits.items[0]?.value_sha256).toBe(
createHash("sha256").update(Buffer.from("e31=", "base64")).digest("hex"),
);
const malformed = await captureCacheFingerprint("%%%not-base64%%%");
expect(malformed.complete).toBe(false);
expect(malformed.items[0]?.value_sha256).toBeNull();
});
it("decodes large JSON response bodies and preserves malformed and pad-bit behavior", () => {
const events = createEvents();
startJsonResponse(events, "large-json", "/large.json");
const encoded = Buffer.from(
JSON.stringify({ value: "x".repeat(BODY_BYTES) }),
).toString("base64");
const candidate = events.responseBodyRequests()[0];
expect(candidate).toBeDefined();
events.ingestResponseBody(candidate!, {
body: encoded,
base64Encoded: true,
});
expect(events.network.get("large-json")?.body_shapes.response).toMatchObject({
root_type: "object",
node_count: 2,
});
startJsonResponse(events, "pad-bits", "/pad-bits.json");
const padBits = events
.responseBodyRequests()
.find(({ requestId }) => requestId === "pad-bits");
expect(padBits).toBeDefined();
events.ingestResponseBody(padBits!, {
body: "e31=",
base64Encoded: true,
});
expect(events.network.get("pad-bits")?.body_shapes.response?.root_type).toBe(
"object",
);
startJsonResponse(events, "malformed", "/malformed.json");
const malformed = events
.responseBodyRequests()
.find(({ requestId }) => requestId === "malformed");
expect(malformed).toBeDefined();
events.ingestResponseBody(malformed!, {
body: "%%%not-base64%%%",
base64Encoded: true,
});
expect(events.network.get("malformed")?.body_shapes.response).toBeNull();
});
it("counts large binary WebSocket frames without accepting malformed base64", () => {
const events = createEvents(false);
const encoded = Buffer.alloc(BODY_BYTES, 0x81).toString("base64");
events.ingest({
method: "Network.webSocketCreated",
params: { requestId: "socket", url: "wss://example.test/socket" },
});
events.ingest({
method: "Network.webSocketFrameReceived",
params: {
requestId: "socket",
response: { opcode: 2, payloadData: encoded },
},
});
expect(
events.webSocketConnectionsById.get("socket")?.events[0]?.payload_bytes,
).toBe(BODY_BYTES);
events.ingest({
method: "Network.webSocketFrameReceived",
params: {
requestId: "socket",
response: { opcode: 2, payloadData: "%%%not-base64%%%" },
},
});
expect(
events.webSocketConnectionsById.get("socket")?.events[1]?.payload_bytes,
).toBeNull();
expect(events.completeness.snapshot().excluded).toContainEqual({
section: "websocket_frames",
reason: "invalid_protocol_value",
count: 1,
});
});
+3 -8
View File
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
import { LosslessNumber } from "lossless-json";
import { parseCaptureJson } from "./CaptureJson.js";
import type { JsonValue } from "../../domain/jsonValue.js";
import { CANONICAL_BASE64_PATTERN } from "../../domain/stringPatterns.js";
import {
WEB_NETWORK_CAPTURE_LIMITS,
type WebNetworkCapture,
@@ -264,17 +265,11 @@ const integerLexeme = (literal: string): boolean => {
const decodeHarBase64 = (value: string, pointer: string): Buffer => {
const compact = value.replace(/[\t\n\r ]/g, "");
const bytes = Buffer.from(compact, "base64");
if (
!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(
compact,
) ||
bytes.toString("base64") !== compact
)
if (compact.length % 4 !== 0 || !CANONICAL_BASE64_PATTERN.test(compact))
throw new CaptureFormatError(
"format",
"HAR content declares base64 but its text is not valid canonical base64 (ASCII whitespace is allowed).",
pointer,
);
return bytes;
return Buffer.from(compact, "base64");
};
+63 -13
View File
@@ -1,5 +1,14 @@
import { describe, expect, it } from "vitest";
import { parseBytePattern, byteSearchResultSchema } from "./bytePattern.js";
import { z } from "zod";
import {
bytePatternFields,
validateBytePattern,
byteSearchResultSchema,
} from "./bytePattern.js";
const patternSchema = z
.strictObject(bytePatternFields)
.superRefine(validateBytePattern);
it.each(["0x20", "overlay:0x10", "invalid"])(
"rejects a match outside reported searched coverage: %s",
@@ -53,15 +62,13 @@ it.each(["", "overlay:"])(
describe("masked byte patterns", () => {
it("preserves whole-byte wildcards and combines explicit bit masks", () => {
const parsed = parseBytePattern("48 8b\t??89", "ff f0 ff 0f");
if (!parsed.ok) throw parsed.error;
expect(parsed.value).toEqual({
bytes: [0x48, 0x8b, 0, 0x89],
masks: [255, 240, 0, 15],
});
const exact = parseBytePattern("Aa\u00a000");
if (!exact.ok) throw exact.error;
expect(exact.value).toEqual({ bytes: [170, 0], masks: [255, 255] });
for (const input of [
{ pattern: "48 8b\t??89", mask: "ff f0 ff 0f" },
{ pattern: "Aa\u00a000" },
{ pattern: "00" },
{ pattern: "????aa", mask: "000001" },
])
expect(patternSchema.parse(input)).toEqual(input);
});
it.each([
@@ -73,11 +80,54 @@ describe("masked byte patterns", () => {
["ff", "00", "pattern"],
["ffff", "ff", "mask"],
["ff", "??", "mask"],
["aa ??", "00 ff", "pattern"],
["??", "0000", "mask"],
["aa", "00gg", "mask"],
["??gg", "00gg", "pattern"],
])("rejects invalid pattern %s with typed issues", (pattern, mask, field) => {
const parsed = parseBytePattern(pattern, mask);
if (parsed.ok) throw new Error("Expected a rejected pattern");
const parsed = patternSchema.safeParse({ pattern, mask });
expect(parsed.success).toBe(false);
if (parsed.success) throw new Error("Expected a rejected pattern");
expect(parsed.error.issues).toMatchObject([
{ path: [field], reason: "invalid_value" },
{ path: [field], code: "custom" },
]);
});
it("accepts ECMAScript whitespace only between complete byte pairs", () => {
const whitespace = [
"\t",
"\n",
"\v",
"\f",
"\r",
" ",
"\u00a0",
"\u1680",
...Array.from({ length: 11 }, (_, i) => String.fromCharCode(0x2000 + i)),
"\u2028",
"\u2029",
"\u202f",
"\u205f",
"\u3000",
"\ufeff",
];
for (const separator of whitespace) {
expect(
patternSchema.safeParse({
pattern: `aa${separator}??`,
mask: `ff${separator}ff`,
}).success,
).toBe(true);
for (const pattern of [
`${separator}aa`,
`aa${separator}`,
`a${separator}a`,
])
expect(patternSchema.safeParse({ pattern }).success).toBe(false);
}
for (const separator of ["\u0085", "\u180e", "\u200b", "\u200c", "\u200d"])
expect(
patternSchema.safeParse({ pattern: `aa${separator}bb` }).success,
).toBe(false);
});
});
+73 -22
View File
@@ -3,45 +3,96 @@ import { addressCoordinate } from "./addressCoordinate.js";
import { AnalysisInputError } from "../analysisErrorCore.js";
import { err, ok, type Result } from "../result.js";
/** Byte pairs, with whole-byte wildcards; explicit masks can select individual bits. */
/** Exact grammar belongs to the object refinement, not a repeating advertised regex. */
export const bytePatternFields = {
pattern: z
.string()
.regex(/^([0-9a-fA-F]{2}|\?\?)(\s*([0-9a-fA-F]{2}|\?\?))*$/u),
.describe(
"Hexadecimal byte pairs or ?? whole-byte wildcards, with optional whitespace between complete pairs; no leading or trailing whitespace. At least one bit must be constrained after applying the mask.",
),
mask: z
.string()
.regex(/^[0-9a-fA-F]{2}(\s*[0-9a-fA-F]{2})*$/u)
.optional(),
.optional()
.describe(
"Hexadecimal mask byte pairs, with optional whitespace between complete pairs; no leading or trailing whitespace. Must contain the same number of bytes as pattern.",
),
};
/** Parse once at the input boundary, retaining typed correction issues. */
export const parseBytePattern = (
const hexDigit = (code: number): number | undefined => {
if (code >= 48 && code <= 57) return code - 48;
if (code >= 65 && code <= 70) return code - 55;
if (code >= 97 && code <= 102) return code - 87;
return undefined;
};
const readBytePair = (
value: string,
offset: number,
): number | "wildcard" | undefined => {
if (value[offset] === "?" && value[offset + 1] === "?") return "wildcard";
const high = hexDigit(value.charCodeAt(offset));
const low = hexDigit(value.charCodeAt(offset + 1));
return high === undefined || low === undefined ? undefined : high * 16 + low;
};
const nextPairOffset = (value: string, offset: number): number => {
const end = offset + 2;
let next = end;
while (next < value.length && /\s/u.test(value[next]!)) next += 1;
return next === value.length && next !== end ? -1 : next;
};
const countBytePairs = (
value: string,
field: "pattern" | "mask",
): number | undefined => {
let count = 0;
for (let offset = 0; offset < value.length;) {
const byte = readBytePair(value, offset);
if (byte === undefined || (field === "mask" && byte === "wildcard"))
return undefined;
count += 1;
offset = nextPairOffset(value, offset);
if (offset < 0) return undefined;
}
return count === 0 ? undefined : count;
};
/** Validate without materializing byte arrays or using size-dependent regexes. */
const validateBytePatternValue = (
pattern: string,
mask?: string,
): Result<{ bytes: number[]; masks: number[] }, AnalysisInputError> => {
): Result<null, AnalysisInputError> => {
const invalid = (field: string, message: string) =>
err(
new AnalysisInputError("search_bytes", undefined, [
{ path: [field], reason: "invalid_value", message },
]),
);
if (!bytePatternFields.pattern.safeParse(pattern).success)
const patternCount = countBytePairs(pattern, "pattern");
if (patternCount === undefined)
return invalid("pattern", "Use hexadecimal byte pairs or ?? wildcards");
if (mask !== undefined && !bytePatternFields.mask.safeParse(mask).success)
const maskCount =
mask === undefined ? undefined : countBytePairs(mask, "mask");
if (mask !== undefined && maskCount === undefined)
return invalid("mask", "Use hexadecimal mask byte pairs");
const pairs = pattern.replace(/\s/gu, "").match(/../gu) ?? [];
const maskPairs = mask?.replace(/\s/gu, "").match(/../gu);
if (maskPairs && maskPairs.length !== pairs.length)
if (maskCount !== undefined && maskCount !== patternCount)
return invalid("mask", "Mask length must equal pattern length");
const bytes = pairs.map((pair) =>
pair === "??" ? 0 : Number.parseInt(pair, 16),
);
const masks = pairs.map((pair, index) =>
pair === "??" ? 0 : Number.parseInt(maskPairs?.[index] ?? "ff", 16),
);
if (!masks.some((value) => value !== 0))
return invalid("pattern", "Pattern must constrain at least one bit");
return ok({ bytes, masks });
let patternOffset = 0;
let maskOffset = 0;
for (let index = 0; index < patternCount; index += 1) {
const byte = readBytePair(pattern, patternOffset);
const selected = mask === undefined ? 255 : readBytePair(mask, maskOffset);
if (
typeof byte === "number" &&
typeof selected === "number" &&
selected !== 0
)
return ok(null);
patternOffset = nextPairOffset(pattern, patternOffset);
if (mask !== undefined) maskOffset = nextPairOffset(mask, maskOffset);
}
return invalid("pattern", "Pattern must constrain at least one bit");
};
/** Shared semantic validation for public and provider inputs. */
@@ -49,7 +100,7 @@ export const validateBytePattern = (
value: { pattern: string; mask?: string | undefined },
context: z.RefinementCtx,
): void => {
const parsed = parseBytePattern(value.pattern, value.mask);
const parsed = validateBytePatternValue(value.pattern, value.mask);
if (!parsed.ok)
for (const issue of parsed.error.issues)
context.addIssue({
+44
View File
@@ -0,0 +1,44 @@
import { expect, it } from "vitest";
import { decodePaddedBase64, paddedBase64ByteLength } from "./paddedBase64.js";
it("preserves padded syntax, including empty bodies and nonzero pad bits", () => {
for (const [encoded, decoded] of [
["", ""],
["AA==", "00"],
["AB==", "00"],
["AAA=", "0000"],
["AAB=", "0000"],
["////", "ffffff"],
] as const) {
expect(decodePaddedBase64(encoded)?.toString("hex")).toBe(decoded);
expect(paddedBase64ByteLength(encoded)).toBe(decoded.length / 2);
}
for (const malformed of [
"A",
"AA",
"AAA",
"=AAA",
"A===",
"====",
"AA==A",
"AA-_",
"AA==\n",
"AA==\r",
"AA== ",
"A A=",
"AA==",
]) {
expect(paddedBase64ByteLength(malformed), malformed).toBeUndefined();
expect(decodePaddedBase64(malformed), malformed).toBeUndefined();
}
});
it("validates large bodies and malformed suffixes without a regexp stack overflow", () => {
const body = Buffer.alloc(24 * 1024 * 1024 - 3, 0x81);
const encoded = body.toString("base64");
expect(paddedBase64ByteLength(encoded)).toBe(body.length);
expect(decodePaddedBase64(encoded)?.equals(body)).toBe(true);
const malformed = `${encoded.slice(0, -1)}!`;
expect(paddedBase64ByteLength(malformed)).toBeUndefined();
});
+14
View File
@@ -0,0 +1,14 @@
import { PADDED_BASE64_PATTERN } from "./stringPatterns.js";
/** Validate padded syntax without imposing canonical zero pad bits. */
export const paddedBase64ByteLength = (value: string): number | undefined => {
if (value.length % 4 !== 0 || !PADDED_BASE64_PATTERN.test(value))
return undefined;
const padding = value.endsWith("==") ? 2 : value.endsWith("=") ? 1 : 0;
return (value.length / 4) * 3 - padding;
};
export const decodePaddedBase64 = (value: string): Buffer | undefined =>
paddedBase64ByteLength(value) === undefined
? undefined
: Buffer.from(value, "base64");
+23
View File
@@ -3,6 +3,7 @@ import { expect, it } from "vitest";
import {
CANONICAL_BASE64_PATTERN,
IDENTIFIER_PATTERN,
PADDED_BASE64_PATTERN,
STABLE_IDENTIFIER_PATTERN,
} from "./stringPatterns.js";
@@ -57,6 +58,11 @@ it("preserves canonical base64 padding and pad-bit validation", () => {
"AAA",
"====",
"AA==\n",
"AA==\r\n",
"AA==\u2028",
"AA==\u2029",
"AA\nAA",
"AA=\n=",
"AA-_",
"a b c",
];
@@ -73,3 +79,20 @@ it("preserves canonical base64 padding and pad-bit validation", () => {
);
}
});
it("validates large canonical base64 under JSON Schema Unicode modes", () => {
// Near the 32 MiB capture input budget: a quartet expansion alone still
// overflows at this size, especially in Unicode-set client validators.
const body = Buffer.alloc(24 * 1024 * 1024 - 3, 0x81);
const encoded = body.toString("base64");
for (const flag of ["", "u", "v"]) {
const pattern = new RegExp(CANONICAL_BASE64_PATTERN.source, flag);
expect(pattern.test(encoded), flag).toBe(true);
expect(pattern.test(`${encoded.slice(0, -1)}!`), flag).toBe(false);
expect(pattern.test(`${encoded.slice(0, -3)}R==`), flag).toBe(false);
expect(pattern.test(`${encoded}A`), flag).toBe(false);
const padded = new RegExp(PADDED_BASE64_PATTERN.source, flag);
expect(padded.test(encoded), flag).toBe(true);
expect(padded.test(`${encoded.slice(0, -1)}!`), flag).toBe(false);
}
});
+13 -2
View File
@@ -4,6 +4,17 @@ export const IDENTIFIER_PATTERN = /^[A-Za-z][A-Za-z0-9._\x2d]*$/u;
/** Stable evidence identifier, also admitting namespace and path separators. */
export const STABLE_IDENTIFIER_PATTERN = /^[A-Za-z][A-Za-z0-9._:\x2f\x2d]*$/u;
// Repeated alphabet quartets exhaust V8's regexp stack on large bodies,
// including under JSON Schema's Unicode-set mode. Check alphabet and terminal
// padding separately, leaving an unconstrained fixed-width loop for length.
const PADDED_BASE64_PREFIX = String.raw`^(?!.*[^A-Za-z0-9+\x2f=])(?!.*=[^=])(?!.*===)`;
/** Padded base64 syntax, allowing nonzero pad bits and the empty encoding. */
export const PADDED_BASE64_PATTERN = new RegExp(
`${PADDED_BASE64_PREFIX}(?:....)*$`,
);
/** Canonical padded base64, including zero pad bits and the empty encoding. */
export const CANONICAL_BASE64_PATTERN =
/^(?:[A-Za-z0-9+\x2f]{4})*(?:[A-Za-z0-9+\x2f][AQgw]==|[A-Za-z0-9+\x2f]{2}[AEIMQUYcgkosw048]=)?$/;
export const CANONICAL_BASE64_PATTERN = new RegExp(
`${PADDED_BASE64_PREFIX}(?!.*[^AQgw]==$)(?!.*[^AEIMQUYcgkosw048=]=$)(?:....)*$`,
);
+24
View File
@@ -5,6 +5,30 @@ import {
parseGhidraInventoryResult,
} from "./GhidraInventoryValues.js";
it("validates large masked byte queries at the provider boundary without changing caller spelling", () => {
const input = {
pattern: "aA??".repeat(1024 * 1024),
mask: "ff00".repeat(1024 * 1024),
};
const parsed = parseGhidraInventoryInput("search_bytes", input);
expect(parsed.ok).toBe(true);
if (!parsed.ok) throw parsed.error;
expect(parsed.value.pattern === input.pattern).toBe(true);
expect(parsed.value.mask === input.mask).toBe(true);
expect(
parseGhidraInventoryInput("search_bytes", {
...input,
mask: `${input.mask.slice(0, -2)}g0`,
}).ok,
).toBe(false);
expect(
parseGhidraInventoryInput("search_bytes", {
...input,
mask: "00ff".repeat(1024 * 1024),
}).ok,
).toBe(false);
});
describe("provider-owned overview boundary", () => {
const overview = {
document: "fixture",
@@ -0,0 +1,138 @@
import { createHash } from "node:crypto";
import { readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect } from "vitest";
import { z } from "zod";
import { parseEvidence } from "../../../src/domain/evidence.js";
import { evidenceBundleSchema } from "../../../src/domain/evidenceBundle.js";
import { webNetworkCaptureSchema } from "../../../src/domain/webNetworkCapture.js";
import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js";
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { historicalHar } from "../../fixtures/historicalHar.js";
import { cliTest } from "../../support/cli/cliFixture.js";
const sha256 = (value: Uint8Array | string): string =>
createHash("sha256").update(value).digest("hex");
cliTest(
"retains a 4 MiB canonical HAR body through CLI and MCP evidence export",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-large-base64-har-");
const path = join(root, "capture.har");
const body = Buffer.alloc(4 * 1024 * 1024, 0x5a);
const contentBase64 = body.toString("base64");
const fixture = historicalHar();
const entry = fixture.log.entries[0];
if (entry === undefined) throw new Error("HAR fixture has no entry");
const original = Buffer.from(
JSON.stringify({
log: {
...fixture.log,
entries: [
{
...entry,
response: {
...entry.response,
content: {
size: body.length,
mimeType: "application/octet-stream",
encoding: "base64",
text: contentBase64,
},
},
},
],
},
}),
);
await writeFile(path, original);
const summarize = (document: unknown) => {
const evidence = parseEvidence(document);
const capture = webNetworkCaptureSchema.parse(evidence.normalized_result);
const record = capture.records[0];
const binary = record?.binary_fields[0];
if (record === undefined || binary === undefined)
throw new Error("HAR evidence omitted its binary sidecar");
if (binary.state !== "retained")
throw new Error(`HAR binary sidecar was ${binary.state}`);
const reported = z
.object({
response: z.object({ content: z.object({ text: z.string() }) }),
})
.parse(record.reported);
const retainedBytes = Buffer.from(binary.content_base64, "base64");
return {
state: binary.state,
bytes: binary.bytes,
sha256: binary.sha256,
contentBase64Length: binary.content_base64.length,
contentBase64Sha256: sha256(binary.content_base64),
decodedLength: retainedBytes.length,
decodedSha256: sha256(retainedBytes),
reportedTextLength: reported.response.content.text.length,
reportedTextSha256: sha256(reported.response.content.text),
};
};
const expected = {
state: "retained",
bytes: body.length,
sha256: sha256(body),
contentBase64Length: contentBase64.length,
contentBase64Sha256: sha256(contentBase64),
decodedLength: body.length,
decodedSha256: sha256(body),
reportedTextLength: contentBase64.length,
reportedTextSha256: sha256(contentBase64),
};
const fromCli = await cli.run({
arguments: ["inspect-web-network-capture", path, "har", "--json"],
timeoutMs: 120_000,
});
expect(fromCli.exitCode).toBe(0);
const cliSummary = summarize(fromCli.json);
const { call } = await connectLocalToolsMcp();
const response = await call("inspect_web_network_capture", {
capture_path: path,
format: "har",
});
const { evidence_reference: reference } = z
.object({
error: z.object({
code: z.literal("resource_constraint"),
details: z.object({
reported_limits: z.object({
boundary: z.literal("mcp-response"),
constraint: z.literal("receive-buffer"),
evidence_reference: z.object({
kind: z.literal("retained-evidence"),
evidence_id: z.string(),
}),
}),
}),
}),
})
.parse(parseMcpToolError(response)).error.details.reported_limits;
const bundlePath = join(root, "evidence.json");
const exported = await call("export_evidence_bundle", { path: bundlePath });
expect(exported.isError).not.toBe(true);
const bundle = evidenceBundleSchema.parse(
JSON.parse(await readFile(bundlePath, "utf8")),
);
const retained = bundle.records.find(
(record) => record.evidence_id === reference.evidence_id,
);
expect(retained).toBeDefined();
const mcpSummary = summarize(retained);
expect(cliSummary).toEqual(expected);
expect(mcpSummary).toEqual(expected);
const unchanged = await readFile(path);
expect(unchanged.byteLength).toBe(original.byteLength);
expect(sha256(unchanged)).toBe(sha256(original));
},
);
@@ -0,0 +1,63 @@
import { expect, it } from "vitest";
import type { CallToolResult } from "@modelcontextprotocol/server";
import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js";
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
const LARGE_PATTERN_BYTES = 4 * 1024 * 1024;
const inputValidationText = (result: CallToolResult): string => {
expect(result.isError).toBe(true);
const text = result.content.find((item) => item.type === "text");
if (text?.type !== "text") throw new Error("Input error omitted text");
expect(text.text).toMatch(
/^Input validation error: Invalid arguments for tool search_bytes:/u,
);
expect(text.text).not.toMatch(/RangeError|Maximum call stack/i);
return text.text;
};
it("validates large search_bytes inputs without breaking stdio MCP", async () => {
const { client, call } = await connectLocalToolsMcp();
const definition = (await client.listTools()).tools.find(
({ name }) => name === "search_bytes",
);
expect(definition?.inputSchema.properties?.pattern).toMatchObject({
type: "string",
});
expect(definition?.inputSchema.properties?.pattern).not.toHaveProperty(
"pattern",
);
expect(definition?.inputSchema.properties?.mask).toMatchObject({
type: "string",
});
expect(definition?.inputSchema.properties?.mask).not.toHaveProperty(
"pattern",
);
const initial = await call("search_bytes", { pattern: "aa" });
expect(parseMcpToolError(initial).error.code).toBe("target_unavailable");
const largePattern = "aa".repeat(LARGE_PATTERN_BYTES);
const largeSearch = await call("search_bytes", { pattern: largePattern });
expect(parseMcpToolError(largeSearch).error.code).toBe("target_unavailable");
const malformedPattern = `${largePattern.slice(0, -2)}a?`;
const malformed = inputValidationText(
await call("search_bytes", { pattern: malformedPattern }),
);
expect(malformed).toMatch(/pattern/i);
const oversizedMask = inputValidationText(
await call("search_bytes", {
pattern: "aa",
mask: "ff".repeat(LARGE_PATTERN_BYTES),
}),
);
expect(oversizedMask).toContain("Mask length must equal pattern length");
const final = await call("search_bytes", { pattern: "aa" });
expect(parseMcpToolError(final).error.code).toBe("target_unavailable");
await client.ping();
});