收紧原生壳载荷边界门禁

导出 HostBridge 文件和通知载荷边界常量
校验移动壳文件大小和 MIME 清单与共享契约一致
校验桌面壳载荷限制和 MIME 清单与共享契约一致
更新原生壳方案和共享决策记录
This commit is contained in:
2026-06-19 00:26:18 +08:00
parent 1f5c752302
commit 759b2cb388
5 changed files with 369 additions and 9 deletions
+126 -1
View File
@@ -140,7 +140,7 @@ function extractStringArrayExport(source, exportName, seen = new Set()) {
}
const match = source.match(
new RegExp(`export const ${exportName}[^=]*= \\[([\\s\\S]*?)\\](?: as const)?;`),
new RegExp(`export const ${exportName}[^=]*= \\[([\\s\\S]*?)\\][^;]*;`),
);
if (!match) {
throw new Error(`unable to read ${exportName}`);
@@ -182,6 +182,52 @@ function extractNumberConstExport(source, exportName) {
return Number(match[1]);
}
function evaluateNumberExpression(expression) {
const tokens = expression
.split('*')
.map((token) => token.trim())
.filter(Boolean);
if (
tokens.length === 0 ||
tokens.some((token) => !/^\d+$/.test(token))
) {
throw new Error(`unsupported numeric expression ${expression}`);
}
return tokens.reduce((value, token) => value * Number(token), 1);
}
function extractNumberExpressionConstExport(source, exportName) {
const match = source.match(
new RegExp(`export const ${exportName}\\s*=\\s*([^;]+);`),
);
if (!match) {
throw new Error(`unable to read ${exportName}`);
}
return evaluateNumberExpression(match[1]);
}
function extractLocalNumberConst(source, constName) {
const match = source.match(new RegExp(`const ${constName}\\s*=\\s*([^;]+);`));
if (!match) {
throw new Error(`unable to read local const ${constName}`);
}
return evaluateNumberExpression(match[1]);
}
function extractStringSetConst(source, constName) {
const match = source.match(
new RegExp(`const ${constName}[^=]*= new Set[^\\[]*\\[([\\s\\S]*?)\\]\\);`),
);
if (!match) {
throw new Error(`unable to read string Set ${constName}`);
}
return [...match[1].matchAll(/'([^']+)'/g)].map((entry) => entry[1]);
}
function extractMobileBridgeHandledMethods(source) {
const match = source.match(
/async function dispatchMobileHostBridgeRequest[\s\S]*?switch \(request\.method\) \{([\s\S]*?)\n \}/,
@@ -487,6 +533,58 @@ const sharedHostBridgeVersion = extractNumberConstExport(
sharedContractSource,
'HOST_BRIDGE_VERSION',
);
const sharedHostBridgePayloadLimits = {
HOST_BRIDGE_EXPORT_TEXT_MAX_BYTES: extractNumberExpressionConstExport(
sharedContractSource,
'HOST_BRIDGE_EXPORT_TEXT_MAX_BYTES',
),
HOST_BRIDGE_IMPORT_TEXT_MAX_BYTES: extractNumberExpressionConstExport(
sharedContractSource,
'HOST_BRIDGE_IMPORT_TEXT_MAX_BYTES',
),
HOST_BRIDGE_EXPORT_IMAGE_MAX_BYTES: extractNumberExpressionConstExport(
sharedContractSource,
'HOST_BRIDGE_EXPORT_IMAGE_MAX_BYTES',
),
HOST_BRIDGE_IMPORT_IMAGE_MAX_BYTES: extractNumberExpressionConstExport(
sharedContractSource,
'HOST_BRIDGE_IMPORT_IMAGE_MAX_BYTES',
),
HOST_BRIDGE_EXPORT_AUDIO_MAX_BYTES: extractNumberExpressionConstExport(
sharedContractSource,
'HOST_BRIDGE_EXPORT_AUDIO_MAX_BYTES',
),
HOST_BRIDGE_IMPORT_AUDIO_MAX_BYTES: extractNumberExpressionConstExport(
sharedContractSource,
'HOST_BRIDGE_IMPORT_AUDIO_MAX_BYTES',
),
};
const mobileHostBridgePayloadLimits = {
HOST_BRIDGE_EXPORT_TEXT_MAX_BYTES: extractLocalNumberConst(
hostBridgeSource,
'EXPORT_TEXT_MAX_BYTES',
),
HOST_BRIDGE_IMPORT_TEXT_MAX_BYTES: extractLocalNumberConst(
hostBridgeSource,
'IMPORT_TEXT_MAX_BYTES',
),
HOST_BRIDGE_EXPORT_IMAGE_MAX_BYTES: extractLocalNumberConst(
hostBridgeSource,
'EXPORT_IMAGE_MAX_BYTES',
),
HOST_BRIDGE_IMPORT_IMAGE_MAX_BYTES: extractLocalNumberConst(
hostBridgeSource,
'IMPORT_IMAGE_MAX_BYTES',
),
HOST_BRIDGE_EXPORT_AUDIO_MAX_BYTES: extractLocalNumberConst(
hostBridgeSource,
'EXPORT_AUDIO_MAX_BYTES',
),
HOST_BRIDGE_IMPORT_AUDIO_MAX_BYTES: extractLocalNumberConst(
hostBridgeSource,
'IMPORT_AUDIO_MAX_BYTES',
),
};
const handledMobileMethods = extractMobileBridgeHandledMethods(dispatchSource);
const mobileCapabilities = extractStringArrayExport(
hostBridgeSource,
@@ -499,6 +597,33 @@ const iosMobileCapabilities = extractStringArrayExport(
const mobileCapabilitySet = new Set(mobileCapabilities);
const iosMobileCapabilitySet = new Set(iosMobileCapabilities);
const sdkBackedCapabilities = ['auth.requestLogin', 'payment.request'];
for (const [limitName, sharedLimit] of Object.entries(
sharedHostBridgePayloadLimits,
)) {
const mobileLimit = mobileHostBridgePayloadLimits[limitName];
if (mobileLimit !== sharedLimit) {
throw new Error(
`mobile shell ${limitName} drifted: expected ${sharedLimit} but got ${mobileLimit}`,
);
}
}
assertSameList(
extractStringSetConst(hostBridgeSource, 'HOST_BRIDGE_TEXT_MIME_TYPES'),
extractStringArrayExport(sharedContractSource, 'HOST_BRIDGE_TEXT_MIME_TYPES'),
'mobile shell text MIME types',
);
assertSameList(
extractStringSetConst(hostBridgeSource, 'HOST_BRIDGE_IMAGE_MIME_TYPES'),
extractStringArrayExport(sharedContractSource, 'HOST_BRIDGE_IMAGE_MIME_TYPES'),
'mobile shell image MIME types',
);
assertSameList(
extractStringSetConst(hostBridgeSource, 'HOST_BRIDGE_AUDIO_MIME_TYPES'),
extractStringArrayExport(sharedContractSource, 'HOST_BRIDGE_AUDIO_MIME_TYPES'),
'mobile shell audio MIME types',
);
const unknownHandledMobileMethods = handledMobileMethods.filter(
(method) => !sharedMethods.includes(method),
);