fix(marketplace): close client review findings

This commit is contained in:
2026-08-29 10:31:38 +08:00
parent 57962591de
commit 3df794c2e7
14 changed files with 840 additions and 68 deletions

View File

@@ -318,20 +318,22 @@ export function verifyMarketplaceClientArtifact(appAsarContents, productionTrust
}
/**
* Read the Marketplace trust source from the packaged application itself.
* Checkout sources are not evidence of what an installed app will trust.
* Read the package.main-reachable application graph from the packaged app.
* Checkout sources and unrelated app.asar files are not product evidence.
*/
export async function readPackagedMarketplaceTrustSource(appAsar) {
async function readPackagedApplicationGraph(appAsar) {
const entries = new Set(listPackage(appAsar, { isPack: false })
.map((entry) => entry.replace(/^[/\\]+/u, '').replaceAll('\\', '/')));
const readArchiveText = (entry) => Buffer.from(
extractFile(appAsar, entry.replaceAll('/', sep)),
).toString('utf8');
const readArchiveText = (entry) => {
if (!entries.has(entry)) throw new Error(`Packaged app.asar entry is missing: ${entry}`);
return Buffer.from(extractFile(appAsar, entry.replaceAll('/', sep))).toString('utf8');
};
let packageJson;
try {
packageJson = JSON.parse(readArchiveText('package.json'));
} catch {
throw new Error('Packaged app.asar package.json is unreadable');
} catch (error) {
const reason = error instanceof Error ? error.message : 'unknown error';
throw new Error(`Packaged app.asar package.json is unreadable: ${reason}`);
}
if (typeof packageJson.main !== 'string' || packageJson.main.trim().length === 0) {
throw new Error('Packaged app.asar package.json.main is missing');
@@ -348,9 +350,10 @@ export async function readPackagedMarketplaceTrustSource(appAsar) {
}
return result.join('/');
};
const resolveModule = (from, specifier) => {
const resolveEntry = (from, specifier) => {
if (!specifier.startsWith('.')) return null;
const base = normalizeAsarPath(`${dirname(from).replaceAll('\\', '/')}/${specifier}`);
const withoutSuffix = specifier.split(/[?#]/u, 1)[0];
const base = normalizeAsarPath(`${dirname(from).replaceAll('\\', '/')}/${withoutSuffix}`);
if (!base) return null;
const candidates = [base, `${base}.js`, `${base}.mjs`, `${base}.cjs`, `${base}/index.js`];
return candidates.find((candidate) => entries.has(candidate)) ?? null;
@@ -372,16 +375,28 @@ export async function readPackagedMarketplaceTrustSource(appAsar) {
}
reachable.push({ entry, source });
const specifiers = [];
const importPattern = /(?:import|export)\s+(?:[\s\S]*?\sfrom\s*)?['"]([^'"]+)['"]/gu;
const importPattern = /\b(?:import|export)(?:[^"'`;]*?\bfrom)?\s*['"]([^'"]+)['"]/gu;
const dynamicImportPattern = /\bimport\(\s*['"]([^'"]+)['"]\s*\)/gu;
const requirePattern = /\brequire\(\s*['"]([^'"]+)['"]\s*\)/gu;
const htmlEntryPattern = /['"]([^'"]+\.html(?:[?#][^'"]*)?)['"]/gu;
const htmlAssetPattern = /\b(?:src|href)\s*=\s*['"]([^'"]+)['"]/gu;
for (const match of source.matchAll(importPattern)) specifiers.push(match[1]);
for (const match of source.matchAll(dynamicImportPattern)) specifiers.push(match[1]);
for (const match of source.matchAll(requirePattern)) specifiers.push(match[1]);
for (const match of source.matchAll(htmlEntryPattern)) specifiers.push(match[1]);
if (entry.endsWith('.html')) {
for (const match of source.matchAll(htmlAssetPattern)) specifiers.push(match[1]);
}
for (const specifier of specifiers) {
const resolved = resolveModule(entry, specifier);
const resolved = resolveEntry(entry, specifier);
if (resolved) pending.push(resolved);
}
}
return { packageJson, reachable };
}
function findPackagedMarketplaceTrustSource(reachable) {
for (const { source } of reachable) {
const trustAssignment = source.match(
/CODE_OWNED_PLUGIN_SIGNING_KEYS\s*=\s*Object\.freeze\(\s*\{\}\s*(?:as\s+[^)]*)?\)/u,
@@ -398,6 +413,31 @@ export async function readPackagedMarketplaceTrustSource(appAsar) {
throw new Error('Packaged app.asar does not contain the Marketplace trust source');
}
/**
* Read the Marketplace trust source from the packaged application itself.
* Checkout sources are not evidence of what an installed app will trust.
*/
export async function readPackagedMarketplaceTrustSource(appAsar) {
const { reachable } = await readPackagedApplicationGraph(appAsar);
return findPackagedMarketplaceTrustSource(reachable);
}
/**
* Verify Marketplace contract markers only in the package.main-reachable app
* graph. An arbitrary string elsewhere in app.asar is not evidence that the
* installed Main/Renderer contract is present.
*/
export async function verifyPackagedMarketplaceClientArtifact(appAsar) {
const { reachable } = await readPackagedApplicationGraph(appAsar);
const reachableContents = Buffer.from(reachable.map(({ source }) => source).join('\n'));
const trustSource = findPackagedMarketplaceTrustSource(reachable);
if (!reachableContents.includes(Buffer.from(CODE_OWNED_PLUGIN_SIGNING_KEYS_SOURCE_MARKER))) {
throw new Error('Packaged app.asar reachable graph does not contain the Marketplace trust provenance marker');
}
const verifiedTrustSource = `${trustSource}\n${CODE_OWNED_PLUGIN_SIGNING_KEYS_SOURCE_MARKER}`;
return verifyMarketplaceClientArtifact(reachableContents, verifiedTrustSource);
}
async function filesContainingNeedles(root, needles) {
const matches = [];
const visit = async (path) => {
@@ -648,7 +688,6 @@ export async function verifyPiProductArtifact({ projectRoot, executable }) {
throw new Error(`Pi runtime manifest contains absolute paths: ${JSON.stringify(absoluteManifestValues)}`);
}
const appAsarContents = await readFile(appAsar);
const marketplaceTrustSource = await readPackagedMarketplaceTrustSource(appAsar);
const physicalOpenCodePaths = await collectForbiddenResourcePaths(resourcesDirectory);
const asarOpenCodePaths = collectForbiddenAsarPaths(appAsar);
const openCodeResourcePaths = classifyOpenCodeResourcePaths([
@@ -666,7 +705,7 @@ export async function verifyPiProductArtifact({ projectRoot, executable }) {
resourcesDirectory,
appAsarContents,
});
const marketplace = verifyMarketplaceClientArtifact(appAsarContents, marketplaceTrustSource);
const marketplace = await verifyPackagedMarketplaceClientArtifact(appAsar);
const actualSkills = bundledPluginResources.coreResources.skills;
const missingExtensionMarkers = EXTENSION_CONTRACT_MARKERS.filter(
(marker) => !appAsarContents.includes(Buffer.from(marker)),