Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled

Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11),
free to diverge. Tree copied sans upstream .git; upstream remote added for
future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19.
Preserves docs/ARCHITECTURE.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
2026-07-05 09:36:54 +00:00
parent 3216769225
commit bedb527145
21108 changed files with 6010766 additions and 0 deletions

View File

@@ -0,0 +1,70 @@
{
"name": "@openclaw/media-core",
"version": "0.0.0-private",
"private": true,
"files": [
"dist"
],
"type": "module",
"main": "./dist/index.mjs",
"types": "./dist/index.d.mts",
"exports": {
".": {
"types": "./dist/index.d.mts",
"import": "./dist/index.mjs",
"default": "./dist/index.mjs"
},
"./base64": {
"types": "./dist/base64.d.mts",
"import": "./dist/base64.mjs",
"default": "./dist/base64.mjs"
},
"./constants": {
"types": "./dist/constants.d.mts",
"import": "./dist/constants.mjs",
"default": "./dist/constants.mjs"
},
"./content-length": {
"types": "./dist/content-length.d.mts",
"import": "./dist/content-length.mjs",
"default": "./dist/content-length.mjs"
},
"./file-name": {
"types": "./dist/file-name.d.mts",
"import": "./dist/file-name.mjs",
"default": "./dist/file-name.mjs"
},
"./inbound-path-policy": {
"types": "./dist/inbound-path-policy.d.mts",
"import": "./dist/inbound-path-policy.mjs",
"default": "./dist/inbound-path-policy.mjs"
},
"./inline-image-data-url": {
"types": "./dist/inline-image-data-url.d.mts",
"import": "./dist/inline-image-data-url.mjs",
"default": "./dist/inline-image-data-url.mjs"
},
"./media-source-url": {
"types": "./dist/media-source-url.d.mts",
"import": "./dist/media-source-url.mjs",
"default": "./dist/media-source-url.mjs"
},
"./mime": {
"types": "./dist/mime.d.mts",
"import": "./dist/mime.mjs",
"default": "./dist/mime.mjs"
},
"./read-byte-stream-with-limit": {
"types": "./dist/read-byte-stream-with-limit.d.mts",
"import": "./dist/read-byte-stream-with-limit.mjs",
"default": "./dist/read-byte-stream-with-limit.mjs"
}
},
"dependencies": {
"@openclaw/normalization-core": "workspace:*",
"file-type": "22.0.1"
},
"scripts": {
"build": "tsdown src/index.ts src/base64.ts src/constants.ts src/content-length.ts src/file-name.ts src/inbound-path-policy.ts src/inline-image-data-url.ts src/media-source-url.ts src/mime.ts src/read-byte-stream-with-limit.ts --no-config --platform node --format esm --dts --out-dir dist --clean"
}
}

View File

@@ -0,0 +1,44 @@
// Media Core tests cover base64 behavior.
import { describe, expect, it } from "vitest";
import { canonicalizeBase64, estimateBase64DecodedBytes } from "./base64.js";
describe("base64 helpers", () => {
function expectBase64HelperCase<T>(actual: T, expected: T) {
expect(actual).toBe(expected);
}
it.each([
{
name: "canonicalizeBase64 normalizes whitespace and keeps valid base64",
actual: canonicalizeBase64(" SGV s bG8= \n"),
expected: "SGVsbG8=",
},
{
name: "canonicalizeBase64 pads valid unpadded base64",
actual: canonicalizeBase64("SGVsbG8"),
expected: "SGVsbG8=",
},
{
name: "canonicalizeBase64 rejects impossible unpadded length",
actual: canonicalizeBase64("S"),
expected: undefined,
},
{
name: "canonicalizeBase64 rejects invalid base64 characters",
actual: canonicalizeBase64('SGVsbG8=" onerror="alert(1)'),
expected: undefined,
},
{
name: "estimateBase64DecodedBytes handles whitespace",
actual: estimateBase64DecodedBytes("SGV s bG8= \n"),
expected: 5,
},
{
name: "estimateBase64DecodedBytes handles empty input",
actual: estimateBase64DecodedBytes(""),
expected: 0,
},
] as const)("$name", ({ actual, expected }) => {
expectBase64HelperCase(actual, expected);
});
});

View File

@@ -0,0 +1,88 @@
/** Estimates decoded bytes without allocating a cleaned copy of the base64 payload. */
export function estimateBase64DecodedBytes(base64: string): number {
// Avoid `trim()`/`replace()` here: they allocate a second (potentially huge) string.
// We only need a conservative decoded-size estimate to enforce budgets before Buffer.from(..., "base64").
let effectiveLen = 0;
for (let i = 0; i < base64.length; i += 1) {
const code = base64.charCodeAt(i);
// Treat ASCII control + space as whitespace; base64 decoders commonly ignore these.
if (code <= 0x20) {
continue;
}
effectiveLen += 1;
}
if (effectiveLen === 0) {
return 0;
}
let padding = 0;
// Find last non-whitespace char(s) to detect '=' padding without allocating/copying.
let end = base64.length - 1;
while (end >= 0 && base64.charCodeAt(end) <= 0x20) {
end -= 1;
}
if (end >= 0 && base64[end] === "=") {
padding = 1;
end -= 1;
while (end >= 0 && base64.charCodeAt(end) <= 0x20) {
end -= 1;
}
if (end >= 0 && base64[end] === "=") {
padding = 2;
}
}
const estimated = Math.floor((effectiveLen * 3) / 4) - padding;
return Math.max(0, estimated);
}
function isBase64DataChar(code: number): boolean {
return (
(code >= 0x41 && code <= 0x5a) ||
(code >= 0x61 && code <= 0x7a) ||
(code >= 0x30 && code <= 0x39) ||
code === 0x2b ||
code === 0x2f
);
}
/**
* Normalizes and validates a base64 string, returning canonical no-whitespace
* base64 only when the input has valid alphabet, padding, and length.
*/
export function canonicalizeBase64(base64: string): string | undefined {
let cleaned = "";
let padding = 0;
let sawPadding = false;
for (let i = 0; i < base64.length; i += 1) {
const code = base64.charCodeAt(i);
if (code <= 0x20) {
continue;
}
if (code === 0x3d) {
padding += 1;
if (padding > 2) {
return undefined;
}
sawPadding = true;
cleaned += "=";
continue;
}
if (sawPadding || !isBase64DataChar(code)) {
return undefined;
}
cleaned += base64[i];
}
if (!cleaned) {
return undefined;
}
const remainder = cleaned.length % 4;
if (remainder !== 0) {
if (sawPadding || remainder === 1) {
return undefined;
}
cleaned += "=".repeat(4 - remainder);
}
return cleaned;
}

View File

@@ -0,0 +1,53 @@
/** Default outbound image payload cap shared by media loaders and adapters. */
export const MAX_IMAGE_BYTES = 6 * 1024 * 1024; // 6MB
/** Default outbound audio payload cap shared by media loaders and adapters. */
export const MAX_AUDIO_BYTES = 16 * 1024 * 1024; // 16MB
/** Default outbound video payload cap shared by media loaders and adapters. */
export const MAX_VIDEO_BYTES = 16 * 1024 * 1024; // 16MB
/** Default outbound document payload cap shared by media loaders and adapters. */
export const MAX_DOCUMENT_BYTES = 100 * 1024 * 1024; // 100MB
/** Media families that share size-policy and MIME-classification behavior. */
export type MediaKind = "image" | "audio" | "video" | "document";
/** Maps a MIME type to the media family used for size limits and routing. */
export function mediaKindFromMime(mime?: string | null): MediaKind | undefined {
if (!mime) {
return undefined;
}
if (mime.startsWith("image/")) {
return "image";
}
if (mime.startsWith("audio/")) {
return "audio";
}
if (mime.startsWith("video/")) {
return "video";
}
if (mime === "application/pdf") {
return "document";
}
if (mime.startsWith("text/")) {
return "document";
}
if (mime.startsWith("application/")) {
return "document";
}
return undefined;
}
/** Returns the default byte cap for a classified media family. */
export function maxBytesForKind(kind: MediaKind): number {
switch (kind) {
case "image":
return MAX_IMAGE_BYTES;
case "audio":
return MAX_AUDIO_BYTES;
case "video":
return MAX_VIDEO_BYTES;
case "document":
return MAX_DOCUMENT_BYTES;
default:
return MAX_DOCUMENT_BYTES;
}
}

View File

@@ -0,0 +1,15 @@
/** Parses a Content-Length header as a safe integer or rejects malformed values. */
export function parseMediaContentLength(raw: string | null): number | null {
if (raw === null) {
return null;
}
const trimmed = raw.trim();
if (!/^\d+$/.test(trimmed)) {
throw new Error(`invalid content-length header: ${raw}`);
}
const size = Number(trimmed);
if (!Number.isSafeInteger(size)) {
throw new Error(`invalid content-length header: ${raw}`);
}
return size;
}

View File

@@ -0,0 +1,19 @@
// Media Core module implements file name behavior.
import path from "node:path";
/** Returns the final filename segment for either POSIX or Windows-style paths. */
export function basenameFromAnyPath(value: string): string {
return path.win32.basename(path.posix.basename(value));
}
/** Returns the extension from the final filename segment of any path flavor. */
export function extnameFromAnyPath(value: string): string {
return path.extname(basenameFromAnyPath(value));
}
/** Returns the extensionless filename from the final segment of any path flavor. */
export function nameFromAnyPath(value: string): string {
const base = basenameFromAnyPath(value);
const ext = path.extname(base);
return path.basename(base, ext);
}

View File

@@ -0,0 +1,75 @@
// Media Core tests cover inbound path policy behavior.
import { describe, expect, it } from "vitest";
import {
isInboundPathAllowed,
isValidInboundPathRootPattern,
mergeInboundPathRoots,
} from "./inbound-path-policy.js";
describe("inbound-path-policy", () => {
function expectInboundRootPatternCase(pattern: string, expected: boolean) {
expect(isValidInboundPathRootPattern(pattern)).toBe(expected);
}
function expectInboundPathAllowedCase(filePath: string, expected: boolean) {
expect(
isInboundPathAllowed({ filePath, roots: ["/Users/*/Library/Messages/Attachments"] }),
).toBe(expected);
}
function expectMergedInboundPathRootsCase(params: {
defaults: string[];
additions: string[];
expected: readonly string[];
}) {
expect(mergeInboundPathRoots(params.defaults, params.additions)).toEqual(params.expected);
}
it.each([
{ pattern: "/Users/*/Library/Messages/Attachments", expected: true },
{ pattern: "/Volumes/relay/attachments", expected: true },
{ pattern: "./attachments", expected: false },
{ pattern: "/Users/**/Attachments", expected: false },
] as const)("validates absolute root pattern %s", ({ pattern, expected }) => {
expectInboundRootPatternCase(pattern, expected);
});
it.each([
{
filePath: "/Users/alice/Library/Messages/Attachments/12/34/ABCDEF/IMG_0001.jpeg",
expected: true,
},
{
filePath: "/etc/passwd",
expected: false,
},
] as const)("matches wildcard roots for %s => $expected", ({ filePath, expected }) => {
expectInboundPathAllowedCase(filePath, expected);
});
it("matches Windows drive roots case-insensitively", () => {
expect(
isInboundPathAllowed({
filePath: "C:\\Users\\Alice\\Library\\Messages\\Attachments\\12\\34\\ABCDEF\\IMG_0001.jpeg",
roots: ["c:/users/*/library/messages/attachments"],
}),
).toBe(true);
});
it.each([
{
name: "normalizes and de-duplicates merged roots",
run: () =>
expectMergedInboundPathRootsCase({
defaults: [
"/Users/*/Library/Messages/Attachments/",
"/Users/*/Library/Messages/Attachments",
],
additions: ["/Volumes/relay/attachments"],
expected: ["/Users/*/Library/Messages/Attachments", "/Volumes/relay/attachments"],
}),
},
] as const)("$name", ({ run }) => {
run();
});
});

View File

@@ -0,0 +1,122 @@
// Media Core module implements inbound path policy behavior.
import path from "node:path";
const WILDCARD_SEGMENT = "*";
const WINDOWS_DRIVE_ABS_RE = /^[A-Za-z]:\//;
const WINDOWS_DRIVE_ROOT_RE = /^[A-Za-z]:$/;
function normalizePosixAbsolutePath(value: string): string | undefined {
const trimmed = value.trim();
if (!trimmed || trimmed.includes("\0")) {
return undefined;
}
// Compare all roots as POSIX-style absolute paths so channel configs can use
// stable patterns even when a source reports Windows separators.
const normalized = path.posix.normalize(trimmed.replaceAll("\\", "/"));
const isAbsolute = normalized.startsWith("/") || WINDOWS_DRIVE_ABS_RE.test(normalized);
if (!isAbsolute || normalized === "/") {
return undefined;
}
const withoutTrailingSlash = normalized.endsWith("/") ? normalized.slice(0, -1) : normalized;
if (WINDOWS_DRIVE_ROOT_RE.test(withoutTrailingSlash)) {
return undefined;
}
return WINDOWS_DRIVE_ABS_RE.test(withoutTrailingSlash)
? withoutTrailingSlash.toLowerCase()
: withoutTrailingSlash;
}
function splitPathSegments(value: string): string[] {
return value.split("/").filter(Boolean);
}
function matchesRootPattern(params: { candidatePath: string; rootPattern: string }): boolean {
const candidateSegments = splitPathSegments(params.candidatePath);
const rootSegments = splitPathSegments(params.rootPattern);
if (candidateSegments.length < rootSegments.length) {
return false;
}
for (let idx = 0; idx < rootSegments.length; idx += 1) {
const expected = rootSegments[idx];
const actual = candidateSegments[idx];
if (expected === WILDCARD_SEGMENT) {
continue;
}
if (expected !== actual) {
return false;
}
}
return true;
}
/** Validates an absolute inbound root pattern with whole-segment wildcards only. */
export function isValidInboundPathRootPattern(value: string): boolean {
const normalized = normalizePosixAbsolutePath(value);
if (!normalized) {
return false;
}
const segments = splitPathSegments(normalized);
if (segments.length === 0) {
return false;
}
return segments.every((segment) => segment === WILDCARD_SEGMENT || !segment.includes("*"));
}
/** Normalizes configured inbound attachment roots, dropping invalid or duplicate patterns. */
export function normalizeInboundPathRoots(roots?: readonly string[]): string[] {
const normalized: string[] = [];
const seen = new Set<string>();
for (const root of roots ?? []) {
if (typeof root !== "string") {
continue;
}
if (!isValidInboundPathRootPattern(root)) {
continue;
}
const candidate = normalizePosixAbsolutePath(root);
if (!candidate || seen.has(candidate)) {
continue;
}
seen.add(candidate);
normalized.push(candidate);
}
return normalized;
}
/** Merges inbound attachment root lists while preserving first-seen priority. */
export function mergeInboundPathRoots(
...rootsLists: Array<readonly string[] | undefined>
): string[] {
const merged: string[] = [];
const seen = new Set<string>();
for (const roots of rootsLists) {
const normalized = normalizeInboundPathRoots(roots);
for (const root of normalized) {
if (seen.has(root)) {
continue;
}
seen.add(root);
merged.push(root);
}
}
return merged;
}
/** Checks whether a candidate inbound media path is covered by configured or fallback roots. */
export function isInboundPathAllowed(params: {
filePath: string;
roots: readonly string[];
fallbackRoots?: readonly string[];
}): boolean {
const candidatePath = normalizePosixAbsolutePath(params.filePath);
if (!candidatePath) {
return false;
}
const roots = normalizeInboundPathRoots(params.roots);
const effectiveRoots =
roots.length > 0 ? roots : normalizeInboundPathRoots(params.fallbackRoots ?? undefined);
if (effectiveRoots.length === 0) {
return false;
}
return effectiveRoots.some((rootPattern) => matchesRootPattern({ candidatePath, rootPattern }));
}

View File

@@ -0,0 +1,11 @@
// Public barrel for media URL, MIME, path, and byte-stream helpers.
export * from "./base64.js";
export * from "./constants.js";
export * from "./content-length.js";
export * from "./file-name.js";
export * from "./inbound-path-policy.js";
export * from "./inline-image-data-url.js";
export * from "./media-source-url.js";
export * from "./mime.js";
export * from "./read-byte-stream-with-limit.js";

View File

@@ -0,0 +1,93 @@
// Media Core tests cover inline image data url behavior.
import { describe, expect, it } from "vitest";
import {
sanitizeInlineImageBase64,
sanitizeInlineImageDataUrl,
sanitizeInlineImageDataUrlForStorage,
sniffInlineImageMime,
} from "./inline-image-data-url.js";
const PNG_1X1 =
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR4nGNgYAAAAAMAASsJTYQAAAAASUVORK5CYII=";
const BMP_HEADER = Buffer.from("BMfixture", "ascii").toString("base64");
const HEIC_HEADER = Buffer.from([
0x00, 0x00, 0x00, 0x18, 0x66, 0x74, 0x79, 0x70, 0x68, 0x65, 0x69, 0x63, 0x00, 0x00, 0x00, 0x00,
0x6d, 0x69, 0x66, 0x31,
]).toString("base64");
const HEIF_HEADER = Buffer.from([
0x00, 0x00, 0x00, 0x18, 0x66, 0x74, 0x79, 0x70, 0x6d, 0x69, 0x66, 0x31, 0x00, 0x00, 0x00, 0x00,
]).toString("base64");
describe("inline image data URL sanitizer", () => {
it("keeps non-data image references unchanged", () => {
expect(sanitizeInlineImageDataUrl("https://example.test/image.png")).toBe(
"https://example.test/image.png",
);
});
it("rejects malformed and non-image data URLs", () => {
expect(sanitizeInlineImageDataUrl("data:image/png;base64")).toBeUndefined();
expect(sanitizeInlineImageDataUrl("data:text/plain;base64,SGVsbG8=")).toBeUndefined();
expect(sanitizeInlineImageDataUrl("data:image/png,SGVsbG8=")).toBeUndefined();
expect(sanitizeInlineImageDataUrl("data:image/png;base64,not base64!")).toBeUndefined();
expect(sanitizeInlineImageDataUrl("data:image/png;base64,SGVsbG8=")).toBeUndefined();
});
it("canonicalizes valid data URLs with sniffed MIME type", () => {
expect(sanitizeInlineImageDataUrl(`data:image/jpeg;base64,\n${PNG_1X1}`)).toBe(
`data:image/png;base64,${PNG_1X1}`,
);
});
it("canonicalizes valid unpadded image data URLs", () => {
const unpaddedPng = PNG_1X1.replace(/=+$/u, "");
expect(sanitizeInlineImageDataUrl(`data:image/png;base64,${unpaddedPng}`)).toBe(
`data:image/png;base64,${PNG_1X1}`,
);
});
it("rejects image data URLs for formats that require conversion before provider transport", () => {
expect(sanitizeInlineImageDataUrl(`data:image/bmp;base64,${BMP_HEADER}`)).toBeUndefined();
expect(sanitizeInlineImageDataUrl(`data:image/heic;base64,${HEIC_HEADER}`)).toBeUndefined();
expect(sanitizeInlineImageDataUrl(`data:image/heif;base64,${HEIF_HEADER}`)).toBeUndefined();
});
it("canonicalizes valid image data URLs for storage without transport allowlist filtering", () => {
expect(sanitizeInlineImageDataUrlForStorage(`data:image/bmp;base64,${BMP_HEADER}`)).toBe(
`data:image/bmp;base64,${BMP_HEADER}`,
);
expect(sanitizeInlineImageDataUrlForStorage(`data:image/heic;base64,${HEIC_HEADER}`)).toBe(
`data:image/heic;base64,${HEIC_HEADER}`,
);
});
it("canonicalizes valid image base64 with sniffed MIME type", () => {
expect(sanitizeInlineImageBase64({ mimeType: "image/jpeg", base64: `\n${PNG_1X1}` })).toEqual({
mimeType: "image/png",
base64: PNG_1X1,
});
expect(
sanitizeInlineImageBase64({ mimeType: "image/png", base64: "SGVsbG8=" }),
).toBeUndefined();
});
it("accepts supported non-browser image signatures", () => {
expect(sanitizeInlineImageBase64({ mimeType: "image/bmp", base64: BMP_HEADER })).toEqual({
mimeType: "image/bmp",
base64: BMP_HEADER,
});
expect(sanitizeInlineImageBase64({ mimeType: "image/heic", base64: HEIC_HEADER })).toEqual({
mimeType: "image/heic",
base64: HEIC_HEADER,
});
expect(sanitizeInlineImageBase64({ mimeType: "image/heif", base64: HEIF_HEADER })).toEqual({
mimeType: "image/heif",
base64: HEIF_HEADER,
});
});
it("sniffs supported inline image signatures", () => {
expect(sniffInlineImageMime(Buffer.from("GIF89a", "ascii"))).toBe("image/gif");
expect(sniffInlineImageMime(Buffer.from([0xff, 0xd8, 0xff]))).toBe("image/jpeg");
});
});

View File

@@ -0,0 +1,191 @@
// Media Core module implements inline image data url behavior.
import { canonicalizeBase64 } from "./base64.js";
/** Prefix used to distinguish inline data URLs from remote/local image references. */
export const INLINE_IMAGE_DATA_URL_PREFIX = "data:";
const IMAGE_SIGNATURES: Array<{
mime: string;
matches: (buffer: Buffer) => boolean;
}> = [
{
mime: "image/png",
matches: (buffer) =>
buffer.length >= 8 &&
buffer[0] === 0x89 &&
buffer[1] === 0x50 &&
buffer[2] === 0x4e &&
buffer[3] === 0x47 &&
buffer[4] === 0x0d &&
buffer[5] === 0x0a &&
buffer[6] === 0x1a &&
buffer[7] === 0x0a,
},
{
mime: "image/jpeg",
matches: (buffer) =>
buffer.length >= 3 && buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff,
},
{
mime: "image/webp",
matches: (buffer) =>
buffer.length >= 12 &&
buffer.subarray(0, 4).toString("ascii") === "RIFF" &&
buffer.subarray(8, 12).toString("ascii") === "WEBP",
},
{
mime: "image/gif",
matches: (buffer) =>
buffer.length >= 6 &&
(buffer.subarray(0, 6).toString("ascii") === "GIF87a" ||
buffer.subarray(0, 6).toString("ascii") === "GIF89a"),
},
{
mime: "image/bmp",
matches: (buffer) => buffer.length >= 2 && buffer[0] === 0x42 && buffer[1] === 0x4d,
},
];
const HEIC_BRANDS = new Set(["heic", "heix", "hevc", "hevx", "heis", "heim", "hevm", "hevs"]);
const HEIF_BRANDS = new Set(["mif1", "msf1"]);
const IMAGE_SIGNATURE_PREFIX_BASE64_CHARS = 128;
const INLINE_IMAGE_DATA_URL_MIMES = new Set(["image/png", "image/jpeg", "image/webp", "image/gif"]);
function startsWithDataUrl(value: string): boolean {
return (
value.slice(0, INLINE_IMAGE_DATA_URL_PREFIX.length).toLowerCase() ===
INLINE_IMAGE_DATA_URL_PREFIX
);
}
function sniffIsoBmffImageMime(buffer: Buffer): string | undefined {
if (buffer.length < 12 || buffer.subarray(4, 8).toString("ascii") !== "ftyp") {
return undefined;
}
const brands = [buffer.subarray(8, 12).toString("ascii")];
for (let offset = 16; offset + 4 <= buffer.length; offset += 4) {
brands.push(buffer.subarray(offset, offset + 4).toString("ascii"));
}
if (brands.some((brand) => HEIC_BRANDS.has(brand))) {
return "image/heic";
}
if (brands.some((brand) => HEIF_BRANDS.has(brand))) {
return "image/heif";
}
return undefined;
}
/** Sniffs supported inline image formats from decoded bytes. */
export function sniffInlineImageMime(buffer: Buffer): string | undefined {
return (
IMAGE_SIGNATURES.find((signature) => signature.matches(buffer))?.mime ??
sniffIsoBmffImageMime(buffer)
);
}
function isImageMimeType(value: string): boolean {
return value.trim().toLowerCase().startsWith("image/");
}
export type SanitizedInlineImageBase64 = {
mimeType: string;
base64: string;
};
/** Canonicalizes trusted inline image base64 and rejects malformed or non-image payloads. */
export function sanitizeInlineImageBase64(params: {
mimeType: string;
base64: string;
}): SanitizedInlineImageBase64 | undefined {
if (!isImageMimeType(params.mimeType)) {
return undefined;
}
const canonicalPayload = canonicalizeBase64(params.base64);
if (!canonicalPayload) {
return undefined;
}
const sniffedMimeType = sniffInlineImageMime(
Buffer.from(canonicalPayload.slice(0, IMAGE_SIGNATURE_PREFIX_BASE64_CHARS), "base64"),
);
if (!sniffedMimeType) {
return undefined;
}
return {
mimeType: sniffedMimeType,
base64: canonicalPayload,
};
}
function parseInlineImageDataUrl(value: string):
| {
metadata: string[];
payload: string;
}
| undefined {
if (!startsWithDataUrl(value)) {
return { metadata: [], payload: value };
}
const commaIndex = value.indexOf(",");
if (commaIndex < 0) {
return undefined;
}
return {
metadata: value
.slice(INLINE_IMAGE_DATA_URL_PREFIX.length, commaIndex)
.split(";")
.map((part) => part.trim()),
payload: value.slice(commaIndex + 1),
};
}
function metadataAllowsImageBase64(metadata: string[]): boolean {
const [mimeType, ...options] = metadata;
return (
mimeType !== undefined &&
isImageMimeType(mimeType) &&
options.some((part) => part.toLowerCase() === "base64")
);
}
function sanitizeInlineImageDataUrlWithAllowedMimes(
imageUrl: string,
allowedMimes?: Set<string>,
): string | undefined {
const parsed = parseInlineImageDataUrl(imageUrl);
if (!parsed) {
return undefined;
}
if (parsed.metadata.length === 0) {
return imageUrl;
}
if (!metadataAllowsImageBase64(parsed.metadata)) {
return undefined;
}
const [mimeType] = parsed.metadata;
const sanitized = sanitizeInlineImageBase64({
mimeType: mimeType ?? "",
base64: parsed.payload,
});
if (!sanitized) {
return undefined;
}
if (allowedMimes && !allowedMimes.has(sanitized.mimeType)) {
return undefined;
}
// Trust the byte signature over caller-supplied metadata before reinlining.
return `data:${sanitized.mimeType};base64,${sanitized.base64}`;
}
/**
* Canonicalizes trusted inline image data URLs for persistence.
* Accepts every image signature supported by `sanitizeInlineImageBase64`.
*/
export function sanitizeInlineImageDataUrlForStorage(imageUrl: string): string | undefined {
return sanitizeInlineImageDataUrlWithAllowedMimes(imageUrl);
}
/** Canonicalizes provider-safe inline image data URLs and rejects unsupported formats. */
export function sanitizeInlineImageDataUrl(imageUrl: string): string | undefined {
return sanitizeInlineImageDataUrlWithAllowedMimes(imageUrl, INLINE_IMAGE_DATA_URL_MIMES);
}

View File

@@ -0,0 +1,41 @@
/** Cached async loader used by runtime boundaries that should import on first use. */
export type LazyPromiseLoader<T> = {
load(): Promise<T>;
clear(): void;
};
/** Controls whether a failed first import stays cached or is retried later. */
export type LazyPromiseLoaderOptions = {
cacheRejections?: boolean;
};
/** Creates a single-flight promise cache around a lazy import or other async loader. */
export function createLazyImportLoader<T>(
load: () => Promise<T>,
options: LazyPromiseLoaderOptions = {},
): LazyPromiseLoader<T> {
let promise: Promise<T> | undefined;
const createPromise = (): Promise<T> => {
const loaded = Promise.resolve().then(load);
if (options.cacheRejections !== true) {
// Failed optional-runtime imports should retry after install/config changes.
void loaded.catch(() => {
if (promise === loaded) {
promise = undefined;
}
});
}
return loaded;
};
return {
async load(): Promise<T> {
promise ??= createPromise();
return await promise;
},
clear(): void {
promise = undefined;
},
};
}

View File

@@ -0,0 +1,13 @@
// Media Core module implements media source url behavior.
const HTTP_URL_RE = /^https?:\/\//i;
const MXC_URL_RE = /^mxc:\/\//i;
const BUFFER_URL_RE = /^buffer:\/\//i;
/** Returns true for remote media URLs that should stay URL-backed instead of local-file-backed. */
export function isPassThroughRemoteMediaSource(value: string | null | undefined): boolean {
const normalized = value?.trim() ?? "";
return (
Boolean(normalized) &&
(HTTP_URL_RE.test(normalized) || MXC_URL_RE.test(normalized) || BUFFER_URL_RE.test(normalized))
);
}

View File

@@ -0,0 +1,350 @@
// Media Core tests cover mime behavior.
import JSZip from "jszip";
import { describe, expect, it } from "vitest";
import { mediaKindFromMime } from "./constants.js";
import {
detectMime,
extensionForMime,
FILE_TYPE_SNIFF_MAX_BYTES,
imageMimeFromFormat,
isAudioFileName,
isGifMedia,
kindFromMime,
mimeTypeFromFilePath,
normalizeMimeType,
sliceMimeSniffBuffer,
} from "./mime.js";
async function makeOoxmlZip(opts: { mainMime: string; partPath: string }): Promise<Buffer> {
const zip = new JSZip();
zip.file(
"[Content_Types].xml",
`<Types><Override PartName="${opts.partPath}" ContentType="${opts.mainMime}.main+xml"/></Types>`,
);
zip.file(opts.partPath.slice(1), "<xml/>");
return await zip.generateAsync({ type: "nodebuffer" });
}
describe("mime detection", () => {
async function expectDetectedMime(params: {
input: Parameters<typeof detectMime>[0];
expected: string;
}) {
expect(await detectMime(params.input)).toBe(params.expected);
}
it.each([
{ format: "jpg", expected: "image/jpeg" },
{ format: "jpeg", expected: "image/jpeg" },
{ format: "png", expected: "image/png" },
{ format: "webp", expected: "image/webp" },
{ format: "gif", expected: "image/gif" },
{ format: "unknown", expected: undefined },
])("maps $format image format", ({ format, expected }) => {
expect(imageMimeFromFormat(format)).toBe(expected);
});
it.each([
{
name: "detects docx from buffer",
mainMime: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
partPath: "/word/document.xml",
expected: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
},
{
name: "detects pptx from buffer",
mainMime: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
partPath: "/ppt/presentation.xml",
expected: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
},
] as const)("$name", async ({ mainMime, partPath, expected }) => {
await expectDetectedMime({
input: {
buffer: await makeOoxmlZip({ mainMime, partPath }),
filePath: "/tmp/file.bin",
},
expected,
});
});
it.each([
{
name: "prefers extension mapping over generic zip",
input: async () => {
const zip = new JSZip();
zip.file("hello.txt", "hi");
return {
buffer: await zip.generateAsync({ type: "nodebuffer" }),
filePath: "/tmp/file.xlsx",
};
},
expected: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
},
{
name: "does not let image extensions override generic zip bytes",
input: async () => {
const zip = new JSZip();
zip.file("hello.txt", "hi");
return {
buffer: await zip.generateAsync({ type: "nodebuffer" }),
filePath: "/tmp/fake.png",
};
},
expected: "application/zip",
},
{
name: "does not let image headers override generic zip bytes",
input: async () => {
const zip = new JSZip();
zip.file("hello.txt", "hi");
return {
buffer: await zip.generateAsync({ type: "nodebuffer" }),
headerMime: "image/png",
};
},
expected: "application/zip",
},
{
name: "uses extension mapping for JavaScript assets",
input: async () => ({
filePath: "/tmp/a2ui.bundle.js",
}),
expected: "text/javascript",
},
{
name: "uses extension mapping for YAML assets",
input: async () => ({
filePath: "/tmp/config.yml",
}),
expected: "application/yaml",
},
{
name: "uses extension mapping for YAML documents",
input: async () => ({
filePath: "/tmp/config.yaml",
}),
expected: "application/yaml",
},
] as const)("$name", async ({ input, expected }) => {
await expectDetectedMime({
input: await input(),
expected,
});
});
it("detects HTML files by extension (no magic bytes)", async () => {
const buf = Buffer.from("<!DOCTYPE html><html><body>test</body></html>");
const mime = await detectMime({ buffer: buf, filePath: "/tmp/report.html" });
expect(mime).toBe("text/html");
});
it("detects .htm files by extension", async () => {
const buf = Buffer.from("<html><body>test</body></html>");
const mime = await detectMime({ buffer: buf, filePath: "/tmp/page.htm" });
expect(mime).toBe("text/html");
});
it("detects XML files by extension", async () => {
const mime = await detectMime({ filePath: "/tmp/data.xml" });
expect(mime).toBe("text/xml");
});
it("detects CSS files by extension", async () => {
const mime = await detectMime({ filePath: "/tmp/style.css" });
expect(mime).toBe("text/css");
});
it("detects AAC from a bare filename when buffer sniffing is inconclusive", async () => {
const mime = await detectMime({ buffer: Buffer.alloc(16), filePath: "voice.aac" });
expect(mime).toBe("audio/aac");
});
it("detects Apple CAF audio by magic bytes when file-type does not recognize the container", async () => {
// CAF files start with the four-byte ASCII tag "caff". `file-type` v22 has
// no native CAF detector, so without the manual magic-byte fallback the
// host-local-media validator drops `afconvert`-produced voice-memo CAFs as
// unknown binary blobs. Regression guard for the iMessage voice-memo
// pre-transcode path.
const buf = Buffer.concat([Buffer.from("caff", "ascii"), Buffer.alloc(60)]);
const mime = await detectMime({ buffer: buf });
expect(mime).toBe("audio/x-caf");
});
it("returns audio/x-caf when extension and CAF magic bytes both agree", async () => {
const buf = Buffer.concat([Buffer.from("caff", "ascii"), Buffer.alloc(60)]);
const mime = await detectMime({ buffer: buf, filePath: "/tmp/voice.caf" });
expect(mime).toBe("audio/x-caf");
});
it("caps dependency sniffing to a bounded prefix", () => {
const small = Buffer.alloc(32);
const large = Buffer.alloc(FILE_TYPE_SNIFF_MAX_BYTES + 16);
expect(sliceMimeSniffBuffer(small)).toBe(small);
expect(sliceMimeSniffBuffer(large)).toHaveLength(FILE_TYPE_SNIFF_MAX_BYTES);
});
});
describe("mimeTypeFromFilePath", () => {
it.each([
{ filePath: "image.bmp", expected: "image/bmp" },
{ filePath: "photo.jpg", expected: "image/jpeg" },
{ filePath: "photo.JPG", expected: "image/jpeg" },
{ filePath: "voice.mp3", expected: "audio/mpeg" },
{ filePath: "voice.wav", expected: "audio/wav" },
{ filePath: "clip.avi", expected: "video/x-msvideo" },
{ filePath: "clip.mkv", expected: "video/x-matroska" },
{ filePath: "clip.webm", expected: "video/webm" },
{ filePath: "clip.flv", expected: "video/x-flv" },
{ filePath: "clip.wmv", expected: "video/x-ms-wmv" },
{ filePath: "debug.log", expected: "text/plain" },
{ filePath: "config.yml", expected: "application/yaml" },
{ filePath: "config.yaml", expected: "application/yaml" },
{ filePath: "page.xml", expected: "text/xml" },
{ filePath: "unknown.bin", expected: undefined },
] as const)("maps $filePath", ({ filePath, expected }) => {
expect(mimeTypeFromFilePath(filePath)).toBe(expected);
});
});
describe("extensionForMime", () => {
function expectMimeExtensionCase(
mime: Parameters<typeof extensionForMime>[0],
expected: ReturnType<typeof extensionForMime>,
) {
expect(extensionForMime(mime)).toBe(expected);
}
it.each([
{ mime: "image/jpeg", expected: ".jpg" },
{ mime: "image/jpg", expected: ".jpg" },
{ mime: "image/bmp", expected: ".bmp" },
{ mime: "image/png", expected: ".png" },
{ mime: "image/svg+xml", expected: ".svg" },
{ mime: "image/webp", expected: ".webp" },
{ mime: "image/gif", expected: ".gif" },
{ mime: "image/heic", expected: ".heic" },
{ mime: "audio/mpeg", expected: ".mp3" },
{ mime: "audio/mp3", expected: ".mp3" },
{ mime: "audio/ogg", expected: ".ogg" },
{ mime: "audio/x-wav", expected: ".wav" },
{ mime: "audio/webm", expected: ".webm" },
{ mime: "audio/x-m4a", expected: ".m4a" },
{ mime: "audio/mp4", expected: ".m4a" },
{ mime: "video/x-msvideo", expected: ".avi" },
{ mime: "video/mp4", expected: ".mp4" },
{ mime: "video/x-matroska", expected: ".mkv" },
{ mime: "video/webm", expected: ".webm" },
{ mime: "video/x-flv", expected: ".flv" },
{ mime: "video/x-ms-wmv", expected: ".wmv" },
{ mime: "video/quicktime", expected: ".mov" },
{ mime: "application/pdf", expected: ".pdf" },
{ mime: "application/yaml", expected: ".yaml" },
{ mime: "text/plain", expected: ".txt" },
{ mime: "text/markdown", expected: ".md" },
{ mime: "text/html", expected: ".html" },
{ mime: "text/xml", expected: ".xml" },
{ mime: "text/css", expected: ".css" },
{ mime: "application/xml", expected: ".xml" },
{ mime: "IMAGE/JPEG", expected: ".jpg" },
{ mime: "Audio/X-M4A", expected: ".m4a" },
{ mime: "Video/QuickTime", expected: ".mov" },
{ mime: "video/unknown", expected: undefined },
{ mime: "application/x-custom", expected: undefined },
{ mime: null, expected: undefined },
{ mime: undefined, expected: undefined },
] as const)("maps $mime to extension", ({ mime, expected }) => {
expectMimeExtensionCase(mime, expected);
});
});
describe("isAudioFileName", () => {
function expectAudioFileNameCase(fileName: string, expected: boolean) {
expect(isAudioFileName(fileName)).toBe(expected);
}
it.each([
{ fileName: "voice.mp3", expected: true },
{ fileName: "voice.caf", expected: true },
{ fileName: "voice.bin", expected: false },
] as const)("matches audio extension for $fileName", ({ fileName, expected }) => {
expectAudioFileNameCase(fileName, expected);
});
});
describe("isGifMedia", () => {
it.each([
{
opts: { contentType: "image/gif; charset=binary" },
expected: true,
},
{
opts: { contentType: " IMAGE/GIF " },
expected: true,
},
{
opts: { contentType: "image/png" },
expected: false,
},
{
opts: { fileName: "animation.GIF" },
expected: true,
},
] as const)("detects GIF media from normalized metadata %#", ({ opts, expected }) => {
expect(isGifMedia(opts)).toBe(expected);
});
});
describe("normalizeMimeType", () => {
function expectNormalizedMimeCase(
input: Parameters<typeof normalizeMimeType>[0],
expected: ReturnType<typeof normalizeMimeType>,
) {
expect(normalizeMimeType(input)).toBe(expected);
}
it.each([
{ input: "Audio/MP4; codecs=mp4a.40.2", expected: "audio/mp4" },
{ input: "image/apng", expected: "image/png" },
{ input: " ", expected: undefined },
{ input: null, expected: undefined },
{ input: undefined, expected: undefined },
] as const)("normalizes $input", ({ input, expected }) => {
expectNormalizedMimeCase(input, expected);
});
});
describe("mediaKindFromMime", () => {
function expectMediaKindCase(
mime: Parameters<typeof mediaKindFromMime>[0],
expected: ReturnType<typeof mediaKindFromMime>,
) {
expect(mediaKindFromMime(mime)).toBe(expected);
}
function expectMimeKindCase(
mime: Parameters<typeof kindFromMime>[0],
expected: ReturnType<typeof kindFromMime>,
) {
expect(kindFromMime(mime)).toBe(expected);
}
it.each([
{ mime: "text/plain", expected: "document" },
{ mime: "text/csv", expected: "document" },
{ mime: "text/html; charset=utf-8", expected: "document" },
{ mime: "model/gltf+json", expected: undefined },
{ mime: null, expected: undefined },
{ mime: undefined, expected: undefined },
] as const)("classifies $mime", ({ mime, expected }) => {
expectMediaKindCase(mime, expected);
});
it.each([
{ mime: " Audio/Ogg; codecs=opus ", expected: "audio" },
{ mime: undefined, expected: undefined },
{ mime: "model/gltf+json", expected: undefined },
] as const)("maps kindFromMime($mime) => $expected", ({ mime, expected }) => {
expectMimeKindCase(mime, expected);
});
});

View File

@@ -0,0 +1,289 @@
// Media Core module implements mime behavior.
import path from "node:path";
import { type MediaKind, mediaKindFromMime } from "./constants.js";
import { createLazyImportLoader } from "./lazy-import.js";
/** Maximum byte prefix passed to dependency MIME sniffers for bounded memory/CPU work. */
export const FILE_TYPE_SNIFF_MAX_BYTES = 1024 * 1024;
// Map common mimes to preferred file extensions.
const EXT_BY_MIME: Record<string, string> = {
"image/heic": ".heic",
"image/heif": ".heif",
"image/bmp": ".bmp",
"image/jpg": ".jpg",
"image/jpeg": ".jpg",
"image/png": ".png",
"image/svg+xml": ".svg",
"image/webp": ".webp",
"image/gif": ".gif",
"audio/ogg": ".ogg",
"audio/mpeg": ".mp3",
"audio/mp3": ".mp3",
"audio/wav": ".wav",
"audio/wave": ".wav",
"audio/x-wav": ".wav",
"audio/flac": ".flac",
"audio/aac": ".aac",
"audio/opus": ".opus",
"audio/webm": ".webm",
"audio/x-m4a": ".m4a",
"audio/mp4": ".m4a",
"audio/x-caf": ".caf",
"video/x-msvideo": ".avi",
"video/mp4": ".mp4",
"video/x-matroska": ".mkv",
"video/webm": ".webm",
"video/x-flv": ".flv",
"video/x-ms-wmv": ".wmv",
"video/quicktime": ".mov",
"application/pdf": ".pdf",
"application/json": ".json",
"application/yaml": ".yaml",
"application/zip": ".zip",
"application/gzip": ".gz",
"application/x-tar": ".tar",
"application/x-7z-compressed": ".7z",
"application/vnd.rar": ".rar",
"application/msword": ".doc",
"application/vnd.ms-excel": ".xls",
"application/vnd.ms-powerpoint": ".ppt",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": ".docx",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": ".xlsx",
"application/vnd.openxmlformats-officedocument.presentationml.presentation": ".pptx",
"text/csv": ".csv",
"text/plain": ".txt",
"text/markdown": ".md",
"text/html": ".html",
"text/xml": ".xml",
"text/css": ".css",
"application/xml": ".xml",
};
function buildMimeByExt(): Record<string, string> {
const byExt: Record<string, string> = {};
for (const [mime, ext] of Object.entries(EXT_BY_MIME)) {
byExt[ext] ??= mime;
}
return byExt;
}
const MIME_BY_EXT: Record<string, string> = {
...buildMimeByExt(),
// Canonical extension mappings for common MIME aliases
".jpg": "image/jpeg",
".mp3": "audio/mpeg",
".wav": "audio/wav",
".webm": "video/webm",
// Additional extension aliases
".jpeg": "image/jpeg",
".js": "text/javascript",
".log": "text/plain",
".htm": "text/html",
".xml": "text/xml",
".yml": "application/yaml",
};
const AUDIO_FILE_EXTENSIONS = new Set([
".aac",
".caf",
".flac",
".m4a",
".mp3",
".oga",
".ogg",
".opus",
".wav",
]);
const fileTypeModuleLoader = createLazyImportLoader(() => import("file-type"));
/** Normalizes MIME strings by dropping parameters, lowercasing, and folding APNG to PNG. */
export function normalizeMimeType(mime?: string | null): string | undefined {
if (!mime) {
return undefined;
}
const cleaned = mime.split(";")[0]?.trim().toLowerCase();
if (cleaned === "image/apng") {
return "image/png";
}
return cleaned || undefined;
}
/** Returns the bounded buffer prefix used for dependency MIME sniffing. */
export function sliceMimeSniffBuffer(buffer: Buffer): Buffer {
if (buffer.byteLength <= FILE_TYPE_SNIFF_MAX_BYTES) {
return buffer;
}
return buffer.subarray(0, FILE_TYPE_SNIFF_MAX_BYTES);
}
async function sniffMime(buffer?: Buffer): Promise<string | undefined> {
if (!buffer) {
return undefined;
}
try {
const { fileTypeFromBuffer } = await fileTypeModuleLoader.load();
const type = await fileTypeFromBuffer(sliceMimeSniffBuffer(buffer));
if (type?.mime) {
return normalizeMimeType(type.mime);
}
} catch {
// fall through to manual magic-byte sniffs
}
return sniffKnownAudioMagic(buffer);
}
// Fallbacks for audio containers `file-type` doesn't recognize natively (e.g.
// Apple's CAF, used by iMessage voice memos when produced by `afconvert`).
// Without this the host-local-media validator drops these buffers as unknown
// binary blobs because the sniff returns undefined, even though the file is
// a valid audio container.
function sniffKnownAudioMagic(buffer: Buffer): string | undefined {
if (buffer.byteLength >= 4 && buffer.toString("ascii", 0, 4) === "caff") {
return "audio/x-caf";
}
return undefined;
}
/** Extracts a lowercase extension from a local path or HTTP URL pathname. */
export function getFileExtension(filePath?: string | null): string | undefined {
if (!filePath) {
return undefined;
}
try {
if (/^https?:\/\//i.test(filePath)) {
const url = new URL(filePath);
return path.extname(url.pathname).toLowerCase() || undefined;
}
} catch {
// fall back to plain path parsing
}
const ext = path.extname(filePath).toLowerCase();
return ext || undefined;
}
/** Maps a file path or URL extension to the preferred MIME type when known. */
export function mimeTypeFromFilePath(filePath?: string | null): string | undefined {
const ext = getFileExtension(filePath);
if (!ext) {
return undefined;
}
return MIME_BY_EXT[ext];
}
/** Returns true when a filename extension is a supported audio container. */
export function isAudioFileName(fileName?: string | null): boolean {
const ext = getFileExtension(fileName);
if (!ext) {
return false;
}
return AUDIO_FILE_EXTENSIONS.has(ext);
}
/** Detects the best MIME type from bytes, file path, and header metadata. */
export function detectMime(opts: {
buffer?: Buffer;
headerMime?: string | null;
filePath?: string;
}): Promise<string | undefined> {
return detectMimeImpl(opts);
}
function isGenericMime(mime?: string): boolean {
if (!mime) {
return true;
}
const m = mime.toLowerCase();
return m === "application/octet-stream" || m === "application/zip";
}
function isImageMime(mime?: string): boolean {
return mediaKindFromMime(normalizeMimeType(mime)) === "image";
}
async function detectMimeImpl(opts: {
buffer?: Buffer;
headerMime?: string | null;
filePath?: string;
}): Promise<string | undefined> {
const ext = getFileExtension(opts.filePath);
const extMime = ext ? MIME_BY_EXT[ext] : undefined;
const headerMime = normalizeMimeType(opts.headerMime);
const sniffed = await sniffMime(opts.buffer);
const sniffedGenericContainer = sniffed && isGenericMime(sniffed);
const trustedExtMime = sniffedGenericContainer && isImageMime(extMime) ? undefined : extMime;
const trustedHeaderMime =
sniffedGenericContainer && isImageMime(headerMime) ? undefined : headerMime;
// Prefer sniffed types, but don't let generic container types override a more
// specific extension mapping (e.g. XLSX vs ZIP).
if (sniffed && (!isGenericMime(sniffed) || !trustedExtMime)) {
return sniffed;
}
if (trustedExtMime) {
return trustedExtMime;
}
if (trustedHeaderMime && !isGenericMime(trustedHeaderMime)) {
return trustedHeaderMime;
}
if (sniffed) {
return sniffed;
}
if (trustedHeaderMime) {
return trustedHeaderMime;
}
return undefined;
}
/** Returns the preferred file extension for a normalized or raw MIME string. */
export function extensionForMime(mime?: string | null): string | undefined {
const normalized = normalizeMimeType(mime);
if (!normalized) {
return undefined;
}
return EXT_BY_MIME[normalized];
}
/** Returns true when content type or filename identifies GIF media. */
export function isGifMedia(opts: {
contentType?: string | null;
fileName?: string | null;
}): boolean {
if (normalizeMimeType(opts.contentType) === "image/gif") {
return true;
}
const ext = getFileExtension(opts.fileName);
return ext === ".gif";
}
/** Maps image format labels from encoders/probes to MIME types. */
export function imageMimeFromFormat(format?: string | null): string | undefined {
if (!format) {
return undefined;
}
switch (format.toLowerCase()) {
case "jpg":
case "jpeg":
return "image/jpeg";
case "heic":
return "image/heic";
case "heif":
return "image/heif";
case "png":
return "image/png";
case "webp":
return "image/webp";
case "gif":
return "image/gif";
default:
return undefined;
}
}
/** Normalizes a MIME string before classifying it into a media family. */
export function kindFromMime(mime?: string | null): MediaKind | undefined {
return mediaKindFromMime(normalizeMimeType(mime));
}

View File

@@ -0,0 +1,27 @@
// Media Core tests cover read byte stream with limit behavior.
import { Readable } from "node:stream";
import { describe, expect, it, vi } from "vitest";
import { readByteStreamWithLimit } from "./read-byte-stream-with-limit.js";
describe("readByteStreamWithLimit", () => {
it("returns concatenated bytes up to the limit", async () => {
const buffer = await readByteStreamWithLimit(Readable.from([Buffer.from("ab"), "cd"]), {
maxBytes: 4,
});
expect(buffer).toEqual(Buffer.from("abcd"));
});
it("throws and destroys node streams after overflow", async () => {
const stream = Readable.from([Buffer.alloc(4), Buffer.alloc(4)]);
const destroySpy = vi.spyOn(stream, "destroy");
await expect(
readByteStreamWithLimit(stream, {
maxBytes: 7,
onOverflow: ({ size, maxBytes }) => new Error(`too large ${size}/${maxBytes}`),
}),
).rejects.toThrow("too large 8/7");
expect(destroySpy).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,82 @@
/** Details passed to byte-stream overflow error factories. */
export type ByteStreamLimitOverflow = {
size: number;
maxBytes: number;
};
/** Options for reading an async byte stream under a hard byte cap. */
export type ReadByteStreamWithLimitOptions = {
maxBytes: number;
onOverflow?: (params: ByteStreamLimitOverflow) => Error;
};
function normalizeByteChunk(chunk: unknown): Buffer {
if (Buffer.isBuffer(chunk)) {
return chunk;
}
if (typeof chunk === "string") {
return Buffer.from(chunk);
}
if (chunk instanceof ArrayBuffer) {
return Buffer.from(chunk);
}
if (ArrayBuffer.isView(chunk)) {
return Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength);
}
throw new TypeError(`Unsupported byte stream chunk: ${typeof chunk}`);
}
function destroyReadableOnOverflow(stream: unknown, err: Error): void {
const readable = stream as {
destroy?: (error?: Error) => unknown;
cancel?: (reason?: unknown) => unknown;
};
// Stop upstream producers immediately after overflow; otherwise large media
// streams can continue buffering after the caller has already failed.
if (typeof readable.destroy === "function") {
try {
readable.destroy(err);
} catch {}
return;
}
if (typeof readable.cancel === "function") {
try {
void readable.cancel(err);
} catch {}
}
}
/** Reads and concatenates an async byte stream, throwing once the byte cap is exceeded. */
export async function readByteStreamWithLimit(
stream: AsyncIterable<unknown>,
opts: ReadByteStreamWithLimitOptions,
): Promise<Buffer> {
const { maxBytes } = opts;
if (!Number.isFinite(maxBytes) || maxBytes < 0) {
throw new RangeError(`maxBytes must be a non-negative finite number: ${maxBytes}`);
}
const onOverflow =
opts.onOverflow ??
((params: ByteStreamLimitOverflow) =>
new Error(`Content too large: ${params.size} bytes (limit: ${params.maxBytes} bytes)`));
const chunks: Buffer[] = [];
let total = 0;
for await (const chunk of stream) {
const buffer = normalizeByteChunk(chunk);
if (buffer.byteLength === 0) {
continue;
}
const nextTotal = total + buffer.byteLength;
if (nextTotal > maxBytes) {
const err = onOverflow({ size: nextTotal, maxBytes });
destroyReadableOnOverflow(stream, err);
throw err;
}
chunks.push(buffer);
total = nextTotal;
}
return Buffer.concat(chunks, total);
}