251 lines
6.5 KiB
JavaScript
251 lines
6.5 KiB
JavaScript
import crypto from 'node:crypto';
|
|
import path from 'node:path';
|
|
|
|
function text(value) {
|
|
return String(value ?? '').trim();
|
|
}
|
|
|
|
function normalizeRelativePath(value) {
|
|
return text(value).replace(/\\/g, '/').replace(/^\/+/, '');
|
|
}
|
|
|
|
function isPublicHtmlPath(value) {
|
|
const normalized = normalizeRelativePath(value);
|
|
return (
|
|
normalized.startsWith('public/') &&
|
|
normalized.toLowerCase().endsWith('.html')
|
|
);
|
|
}
|
|
|
|
function issue(code, message, details = {}) {
|
|
return { code, message, ...details };
|
|
}
|
|
|
|
export function buildPublicHtmlArtifactId({
|
|
userId,
|
|
sessionId,
|
|
relativePath,
|
|
}) {
|
|
const hash = crypto
|
|
.createHash('sha256')
|
|
.update(
|
|
`${text(userId)}:${text(sessionId)}:${normalizeRelativePath(relativePath)}`,
|
|
)
|
|
.digest('hex')
|
|
.slice(0, 16);
|
|
return `ca_public_html_${hash}`;
|
|
}
|
|
|
|
function artifactHasReadableReference(artifact) {
|
|
return Boolean(
|
|
text(artifact?.storage_key) ||
|
|
text(artifact?.storageKey) ||
|
|
text(artifact?.canonical_url) ||
|
|
text(artifact?.canonicalUrl),
|
|
);
|
|
}
|
|
|
|
function artifactAssetId(artifact) {
|
|
return text(artifact?.asset_id ?? artifact?.assetId);
|
|
}
|
|
|
|
function artifactKind(artifact) {
|
|
return text(artifact?.artifact_kind ?? artifact?.artifactKind);
|
|
}
|
|
|
|
function artifactDisplayName(artifact) {
|
|
return text(artifact?.display_name ?? artifact?.displayName);
|
|
}
|
|
|
|
function assetRelativePath(asset) {
|
|
return normalizeRelativePath(
|
|
asset?.workspace_relative_path ??
|
|
asset?.workspaceRelativePath,
|
|
);
|
|
}
|
|
|
|
function assetSize(asset) {
|
|
const value = Number(asset?.size_bytes ?? asset?.sizeBytes);
|
|
return Number.isFinite(value) ? value : null;
|
|
}
|
|
|
|
function assetUpdatedAt(asset, fallback) {
|
|
const value = Number(asset?.updated_at ?? asset?.updatedAt);
|
|
return Number.isFinite(value) ? value : fallback;
|
|
}
|
|
|
|
function packageUserId(packageRecord) {
|
|
return text(packageRecord?.user_id ?? packageRecord?.userId);
|
|
}
|
|
|
|
function packageSessionId(packageRecord) {
|
|
return text(packageRecord?.session_id ?? packageRecord?.sessionId);
|
|
}
|
|
|
|
function packageId(packageRecord) {
|
|
return text(packageRecord?.id ?? packageRecord?.packageId);
|
|
}
|
|
|
|
function publicHtmlKey(relativePath) {
|
|
return path.posix.basename(normalizeRelativePath(relativePath));
|
|
}
|
|
|
|
function canonicalPublicUrl({
|
|
publicBaseUrl,
|
|
userId,
|
|
relativePath,
|
|
}) {
|
|
const base = text(publicBaseUrl).replace(/\/+$/, '');
|
|
const owner = encodeURIComponent(text(userId));
|
|
const encodedRelative = normalizeRelativePath(relativePath)
|
|
.split('/')
|
|
.map(encodeURIComponent)
|
|
.join('/');
|
|
return `${base}/MindSpace/${owner}/${encodedRelative}`;
|
|
}
|
|
|
|
export function auditConversationPackages({
|
|
packages = [],
|
|
artifacts = [],
|
|
assets = [],
|
|
publicBaseUrl = 'http://127.0.0.1:5173',
|
|
} = {}) {
|
|
const issues = [];
|
|
const repairs = [];
|
|
const artifactsByPackage = new Map();
|
|
const assetsById = new Map(
|
|
assets.map((asset) => [text(asset?.id), asset]),
|
|
);
|
|
|
|
for (const artifact of artifacts) {
|
|
const id = text(artifact?.package_id ?? artifact?.packageId);
|
|
if (!id) continue;
|
|
const list = artifactsByPackage.get(id) ?? [];
|
|
list.push(artifact);
|
|
artifactsByPackage.set(id, list);
|
|
}
|
|
|
|
for (const packageRecord of packages) {
|
|
const id = packageId(packageRecord);
|
|
const userId = packageUserId(packageRecord);
|
|
const sessionId = packageSessionId(packageRecord);
|
|
const packageArtifacts = artifactsByPackage.get(id) ?? [];
|
|
if (packageArtifacts.length === 0) {
|
|
issues.push(
|
|
issue(
|
|
'package_without_artifacts',
|
|
`package has no artifacts: ${id}`,
|
|
{ packageId: id, userId, sessionId },
|
|
),
|
|
);
|
|
continue;
|
|
}
|
|
|
|
const publicHtmlArtifactNames = new Set(
|
|
packageArtifacts
|
|
.filter((artifact) => artifactKind(artifact) === 'public_html')
|
|
.map((artifact) => artifactDisplayName(artifact))
|
|
.filter(Boolean),
|
|
);
|
|
|
|
for (const artifact of packageArtifacts) {
|
|
const artifactId = text(artifact?.id);
|
|
if (!artifactHasReadableReference(artifact)) {
|
|
issues.push(
|
|
issue(
|
|
'artifact_without_backing_reference',
|
|
`artifact has no storageKey or canonicalUrl: ${artifactId}`,
|
|
{ packageId: id, artifactId },
|
|
),
|
|
);
|
|
}
|
|
|
|
const assetId = artifactAssetId(artifact);
|
|
const asset = assetId ? assetsById.get(assetId) : null;
|
|
if (assetId && !asset) {
|
|
issues.push(
|
|
issue(
|
|
'artifact_asset_missing',
|
|
`artifact points to a missing asset: ${assetId}`,
|
|
{ packageId: id, artifactId, assetId },
|
|
),
|
|
);
|
|
continue;
|
|
}
|
|
|
|
const relativePath = asset ? assetRelativePath(asset) : '';
|
|
if (
|
|
artifactKind(artifact) === 'generated_file' &&
|
|
isPublicHtmlPath(relativePath)
|
|
) {
|
|
const displayName = publicHtmlKey(relativePath);
|
|
if (!publicHtmlArtifactNames.has(displayName)) {
|
|
const repair = {
|
|
action: 'insert_public_html_artifact',
|
|
packageId: id,
|
|
userId,
|
|
sessionId,
|
|
sourceArtifactId: artifactId,
|
|
sourceAssetId: assetId,
|
|
relativePath,
|
|
artifactId: buildPublicHtmlArtifactId({
|
|
userId,
|
|
sessionId,
|
|
relativePath,
|
|
}),
|
|
displayName,
|
|
mimeType: 'text/html',
|
|
sizeBytes:
|
|
assetSize(asset) ??
|
|
(Number(artifact?.size_bytes ?? 0) || null),
|
|
canonicalUrl: canonicalPublicUrl({
|
|
publicBaseUrl,
|
|
userId,
|
|
relativePath,
|
|
}),
|
|
sortOrder: assetUpdatedAt(
|
|
asset,
|
|
Number(artifact?.created_at ?? Date.now()),
|
|
),
|
|
};
|
|
issues.push(
|
|
issue(
|
|
'missing_public_html_artifact',
|
|
`generated public HTML is missing public_html artifact: ${relativePath}`,
|
|
{
|
|
packageId: id,
|
|
artifactId,
|
|
assetId,
|
|
relativePath,
|
|
repair,
|
|
},
|
|
),
|
|
);
|
|
repairs.push(repair);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
ok: issues.length === 0,
|
|
issues,
|
|
repairs,
|
|
summary: {
|
|
packageCount: packages.length,
|
|
artifactCount: artifacts.length,
|
|
assetCount: assets.length,
|
|
issueCount: issues.length,
|
|
repairCount: repairs.length,
|
|
},
|
|
};
|
|
}
|
|
|
|
export const conversationPackageAuditInternals = {
|
|
artifactHasReadableReference,
|
|
canonicalPublicUrl,
|
|
isPublicHtmlPath,
|
|
normalizeRelativePath,
|
|
publicHtmlKey,
|
|
};
|