Files
memind/mindspace-conversation-package-verify.mjs

84 lines
3.0 KiB
JavaScript

function issue(code, message, details = {}) {
return { code, message, ...details };
}
function isReadableUrl(value) {
const text = String(value ?? '').trim();
return (
text.startsWith('http://') ||
text.startsWith('https://') ||
text.startsWith('/api/') ||
text.startsWith('/MindSpace/')
);
}
function normalizeStorageKey(value) {
return String(value ?? '').trim().replace(/^\/+/, '');
}
export async function verifyConversationPackageManifest(manifest, options = {}) {
const issues = [];
const statObject = typeof options.statObject === 'function' ? options.statObject : null;
const packageId = String(manifest?.packageId ?? '').trim();
const storagePrefix = normalizeStorageKey(manifest?.storagePrefix);
const artifacts = Array.isArray(manifest?.artifacts) ? manifest.artifacts : null;
if (!packageId) issues.push(issue('missing_package_id', 'manifest.packageId is required'));
if (!String(manifest?.userId ?? '').trim()) issues.push(issue('missing_user_id', 'manifest.userId is required'));
if (!String(manifest?.sessionId ?? '').trim()) {
issues.push(issue('missing_session_id', 'manifest.sessionId is required'));
}
if (!storagePrefix) issues.push(issue('missing_storage_prefix', 'manifest.storagePrefix is required'));
if (!artifacts) issues.push(issue('invalid_artifacts', 'manifest.artifacts must be an array'));
const seenArtifactIds = new Set();
for (const artifact of artifacts ?? []) {
const artifactId = String(artifact?.artifactId ?? '').trim();
if (!artifactId) {
issues.push(issue('missing_artifact_id', 'artifact.artifactId is required'));
continue;
}
if (seenArtifactIds.has(artifactId)) {
issues.push(issue('duplicate_artifact_id', `duplicate artifact id: ${artifactId}`, { artifactId }));
}
seenArtifactIds.add(artifactId);
const canonicalUrl = String(artifact?.canonicalUrl ?? '').trim();
const storageKey = normalizeStorageKey(artifact?.storageKey);
if (!canonicalUrl && !storageKey) {
issues.push(
issue('artifact_without_backing_reference', 'artifact must have canonicalUrl or storageKey', { artifactId }),
);
}
if (canonicalUrl && !isReadableUrl(canonicalUrl)) {
issues.push(issue('invalid_canonical_url', `artifact canonicalUrl is not readable: ${canonicalUrl}`, {
artifactId,
canonicalUrl,
}));
}
if (storageKey && storagePrefix && !storageKey.startsWith(`${storagePrefix}/`)) {
issues.push(issue('storage_key_outside_package', 'artifact storageKey is outside manifest.storagePrefix', {
artifactId,
storageKey,
storagePrefix,
}));
}
if (storageKey && statObject) {
const stat = await statObject(storageKey);
if (!stat?.exists) {
issues.push(issue('missing_backing_object', `artifact backing object is missing: ${storageKey}`, {
artifactId,
storageKey,
}));
}
}
}
return { ok: issues.length === 0, issues };
}
export const conversationPackageVerifyInternals = {
isReadableUrl,
normalizeStorageKey,
};