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, };