直连移动壳载荷边界契约

移动壳文件能力直接导入共享 HostBridge MIME 和大小边界
移动壳配置检查禁止重新本地声明文件载荷边界
更新原生壳方案和共享决策记录
This commit is contained in:
2026-06-19 00:36:28 +08:00
parent 759b2cb388
commit bb85986d1f
4 changed files with 74 additions and 165 deletions
+33 -43
View File
@@ -17,41 +17,31 @@ import {
type HostBridgeError,
type HostBridgeImageMimeType,
type HostBridgeTextMimeType,
HOST_BRIDGE_AUDIO_MIME_TYPES,
HOST_BRIDGE_EXPORT_AUDIO_MAX_BYTES,
HOST_BRIDGE_EXPORT_IMAGE_MAX_BYTES,
HOST_BRIDGE_EXPORT_TEXT_MAX_BYTES,
HOST_BRIDGE_IMAGE_MIME_TYPES,
HOST_BRIDGE_IMPORT_AUDIO_MAX_BYTES,
HOST_BRIDGE_IMPORT_IMAGE_MAX_BYTES,
HOST_BRIDGE_IMPORT_TEXT_MAX_BYTES,
HOST_BRIDGE_TEXT_MIME_TYPES,
normalizeHostBridgeExportFileName,
} from '../../../../packages/shared/src/contracts/hostBridge';
import { invalidRequest } from './protocol';
const EXPORT_TEXT_MAX_BYTES = 5 * 1024 * 1024;
const EXPORT_IMAGE_MAX_BYTES = 5 * 1024 * 1024;
const EXPORT_AUDIO_MAX_BYTES = 20 * 1024 * 1024;
const IMPORT_TEXT_MAX_BYTES = 5 * 1024 * 1024;
const IMPORT_IMAGE_MAX_BYTES = 10 * 1024 * 1024;
const IMPORT_AUDIO_MAX_BYTES = 20 * 1024 * 1024;
const HOST_BRIDGE_TEXT_MIME_TYPES = new Set<HostBridgeTextMimeType>([
'text/plain',
'text/markdown',
'text/csv',
'application/json',
]);
const HOST_BRIDGE_IMAGE_MIME_TYPES = new Set<HostBridgeImageMimeType>([
'image/png',
'image/jpeg',
'image/webp',
]);
const HOST_BRIDGE_AUDIO_MIME_TYPES = new Set<HostBridgeAudioMimeType>([
'audio/mpeg',
'audio/mp4',
'audio/wav',
'audio/ogg',
'audio/webm',
]);
const HOST_BRIDGE_TEXT_MIME_TYPE_SET = new Set<HostBridgeTextMimeType>(
HOST_BRIDGE_TEXT_MIME_TYPES,
);
const HOST_BRIDGE_IMAGE_MIME_TYPE_SET = new Set<HostBridgeImageMimeType>(
HOST_BRIDGE_IMAGE_MIME_TYPES,
);
const HOST_BRIDGE_AUDIO_MIME_TYPE_SET = new Set<HostBridgeAudioMimeType>(
HOST_BRIDGE_AUDIO_MIME_TYPES,
);
export const MOBILE_AUDIO_DOCUMENT_PICKER_TYPES = [
'audio/*',
'audio/mpeg',
'audio/mp4',
'audio/wav',
'audio/ogg',
'audio/webm',
...HOST_BRIDGE_AUDIO_MIME_TYPES,
];
const BASE64_ALPHABET =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
@@ -247,7 +237,7 @@ export async function exportTextFile(
}
const bytes = utf8ByteLength(content);
if (bytes > EXPORT_TEXT_MAX_BYTES) {
if (bytes > HOST_BRIDGE_EXPORT_TEXT_MAX_BYTES) {
throw invalidRequest('content exceeds file export size limit');
}
@@ -282,7 +272,7 @@ function normalizeImportedTextMimeType(
): HostBridgeTextMimeType | null {
if (typeof value === 'string') {
const mimeType = value.toLowerCase();
if (HOST_BRIDGE_TEXT_MIME_TYPES.has(mimeType as HostBridgeTextMimeType)) {
if (HOST_BRIDGE_TEXT_MIME_TYPE_SET.has(mimeType as HostBridgeTextMimeType)) {
return mimeType as HostBridgeTextMimeType;
}
}
@@ -331,7 +321,7 @@ export async function importTextFile(): Promise<FileImportTextResult> {
}
if (
typeof asset.size === 'number' &&
(asset.size <= 0 || asset.size > IMPORT_TEXT_MAX_BYTES)
(asset.size <= 0 || asset.size > HOST_BRIDGE_IMPORT_TEXT_MAX_BYTES)
) {
throw invalidRequest('text exceeds file import size limit');
}
@@ -339,7 +329,7 @@ export async function importTextFile(): Promise<FileImportTextResult> {
const file = new File(asset.uri);
const content = await file.text();
const bytes = utf8ByteLength(content);
if (bytes <= 0 || bytes > IMPORT_TEXT_MAX_BYTES) {
if (bytes <= 0 || bytes > HOST_BRIDGE_IMPORT_TEXT_MAX_BYTES) {
throw invalidRequest('text exceeds file import size limit');
}
@@ -359,7 +349,7 @@ export async function exportImageFile(
const mimeType = exportPayload?.mimeType;
if (
typeof mimeType !== 'string' ||
!HOST_BRIDGE_IMAGE_MIME_TYPES.has(mimeType as HostBridgeImageMimeType)
!HOST_BRIDGE_IMAGE_MIME_TYPE_SET.has(mimeType as HostBridgeImageMimeType)
) {
throw invalidRequest('mimeType must be an allowed image type');
}
@@ -369,7 +359,7 @@ export async function exportImageFile(
throw invalidRequest('base64Data is required');
}
const bytes = base64DecodedByteLength(base64Data);
if (bytes > EXPORT_IMAGE_MAX_BYTES) {
if (bytes > HOST_BRIDGE_EXPORT_IMAGE_MAX_BYTES) {
throw invalidRequest('image exceeds file export size limit');
}
ensureImageBytesMatchMimeType(base64Data, mimeType as HostBridgeImageMimeType);
@@ -409,7 +399,7 @@ function normalizeImportedImageMimeType(
}
const mimeType = value.toLowerCase();
return HOST_BRIDGE_IMAGE_MIME_TYPES.has(mimeType as HostBridgeImageMimeType)
return HOST_BRIDGE_IMAGE_MIME_TYPE_SET.has(mimeType as HostBridgeImageMimeType)
? (mimeType as HostBridgeImageMimeType)
: null;
}
@@ -451,13 +441,13 @@ function imagePickerResultToImportPayload(
}
const bytes = base64DecodedByteLength(base64Data);
if (bytes <= 0 || bytes > IMPORT_IMAGE_MAX_BYTES) {
if (bytes <= 0 || bytes > HOST_BRIDGE_IMPORT_IMAGE_MAX_BYTES) {
throw invalidRequest('image exceeds file import size limit');
}
ensureImageBytesMatchMimeType(base64Data, mimeType);
if (
typeof asset.fileSize === 'number' &&
asset.fileSize > IMPORT_IMAGE_MAX_BYTES
asset.fileSize > HOST_BRIDGE_IMPORT_IMAGE_MAX_BYTES
) {
throw invalidRequest('image exceeds file import size limit');
}
@@ -547,7 +537,7 @@ function normalizeImportedAudioMimeType(
): HostBridgeAudioMimeType | null {
if (typeof value === 'string') {
const mimeType = value.toLowerCase();
if (HOST_BRIDGE_AUDIO_MIME_TYPES.has(mimeType as HostBridgeAudioMimeType)) {
if (HOST_BRIDGE_AUDIO_MIME_TYPE_SET.has(mimeType as HostBridgeAudioMimeType)) {
return mimeType as HostBridgeAudioMimeType;
}
}
@@ -579,7 +569,7 @@ export async function exportAudioFile(
const mimeType = exportPayload?.mimeType;
if (
typeof mimeType !== 'string' ||
!HOST_BRIDGE_AUDIO_MIME_TYPES.has(mimeType as HostBridgeAudioMimeType)
!HOST_BRIDGE_AUDIO_MIME_TYPE_SET.has(mimeType as HostBridgeAudioMimeType)
) {
throw invalidRequest('mimeType must be an allowed audio type');
}
@@ -589,7 +579,7 @@ export async function exportAudioFile(
throw invalidRequest('base64Data is required');
}
const bytes = base64DecodedByteLength(base64Data);
if (bytes <= 0 || bytes > EXPORT_AUDIO_MAX_BYTES) {
if (bytes <= 0 || bytes > HOST_BRIDGE_EXPORT_AUDIO_MAX_BYTES) {
throw invalidRequest('audio exceeds file export size limit');
}
ensureAudioBytesMatchMimeType(base64Data, mimeType as HostBridgeAudioMimeType);
@@ -648,7 +638,7 @@ export async function importAudioFile(): Promise<FileImportAudioResult> {
}
if (
typeof asset.size === 'number' &&
(asset.size <= 0 || asset.size > IMPORT_AUDIO_MAX_BYTES)
(asset.size <= 0 || asset.size > HOST_BRIDGE_IMPORT_AUDIO_MAX_BYTES)
) {
throw invalidRequest('audio exceeds file import size limit');
}
@@ -659,7 +649,7 @@ export async function importAudioFile(): Promise<FileImportAudioResult> {
throw invalidRequest('base64Data is required');
}
const bytes = base64DecodedByteLength(base64Data);
if (bytes <= 0 || bytes > IMPORT_AUDIO_MAX_BYTES) {
if (bytes <= 0 || bytes > HOST_BRIDGE_IMPORT_AUDIO_MAX_BYTES) {
throw invalidRequest('audio exceeds file import size limit');
}
ensureAudioBytesMatchMimeType(base64Data, mimeType);