/**
 * Base types and interfaces for asset management
 */
import { assetUploadExtensions } from '../../core/format-registry.js';
/**
 * Content type mappings.
 *
 * TODO(import-refactor): this map + `detectAssetKind` are a second source of
 * truth for per-extension MIME / kind alongside the format registry
 * (`src/core/format-registry.ts`, which owns `mimeTypes`). Folding them into
 * the registry is deferred because it would change observable behavior the
 * P0 parity refactor must preserve (e.g. svg's asset_kind `other`→`image`,
 * tsv's Content-Type `octet-stream`→`text/tab-separated-values`). Until that
 * unification lands, `format-registry.test.ts` guards the two against drift
 * on every shared extension.
 */
export const CONTENT_TYPES = {
    // Images
    png: 'image/png',
    jpg: 'image/jpeg',
    jpeg: 'image/jpeg',
    gif: 'image/gif',
    webp: 'image/webp',
    svg: 'image/svg+xml',
    ico: 'image/x-icon',
    // Videos
    mp4: 'video/mp4',
    mov: 'video/quicktime',
    webm: 'video/webm',
    avi: 'video/x-msvideo',
    // Audio
    mp3: 'audio/mpeg',
    wav: 'audio/wav',
    ogg: 'audio/ogg',
    m4a: 'audio/mp4',
    // Documents
    pdf: 'application/pdf',
    doc: 'application/msword',
    docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
    xls: 'application/vnd.ms-excel',
    xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
    ppt: 'application/vnd.ms-powerpoint',
    pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
    // Archives
    zip: 'application/zip',
    tar: 'application/x-tar',
    gz: 'application/gzip',
    // Fonts
    woff: 'font/woff',
    woff2: 'font/woff2',
    ttf: 'font/ttf',
    otf: 'font/otf',
    // Design files
    fig: 'application/octet-stream',
    sketch: 'application/octet-stream',
    // Text
    txt: 'text/plain',
    json: 'application/json',
    xml: 'application/xml',
    csv: 'text/csv',
};
/**
 * Detect content type from filename
 */
export function detectContentType(filename) {
    const ext = filename.split('.').pop()?.toLowerCase() || '';
    return CONTENT_TYPES[ext] || 'application/octet-stream';
}
/**
 * Map a filename to the high-level `asset_kind` written into the asset
 * entity's frontmatter. Drives icon and viewer selection; coarser than
 * MIME so the UI doesn't need to enumerate every format. The kind set
 * is canonical in `ASSET_KINDS` in src/core/validation.ts.
 */
export function detectAssetKind(filename) {
    const ext = filename.split('.').pop()?.toLowerCase() || '';
    if (IMAGE_EXTS.has(ext))
        return 'image';
    if (VIDEO_EXTS.has(ext))
        return 'video';
    if (AUDIO_EXTS.has(ext))
        return 'audio';
    if (ext === 'pdf')
        return 'pdf';
    if (SPREADSHEET_EXTS.has(ext))
        return 'spreadsheet';
    return 'other';
}
// SVG is intentionally excluded — see docs/asset-architecture.md §security.
// Stored XSS risk: SVG can contain <script> and the GET /api/assets/* route
// serves files inline with `Content-Type: image/svg+xml`, which executes in
// the app origin. Users who need vector assets can paste source into a note
// or embed via the editor's per-entity attach (which also blocks SVG).
const IMAGE_EXTS = new Set(['jpg', 'jpeg', 'png', 'gif', 'webp', 'avif', 'heic']);
const VIDEO_EXTS = new Set(['mp4', 'mov', 'webm', 'avi', 'mkv', 'm4v']);
const AUDIO_EXTS = new Set(['mp3', 'wav', 'm4a', 'ogg', 'flac', 'aac']);
const SPREADSHEET_EXTS = new Set(['csv', 'xlsx', 'xls', 'numbers', 'tsv']);
// Other formerly-categorized extensions (indd, sketch, psd, ai, fig, doc/docx,
// pptx, key, pages, zip, tar, etc.) all fall to `other` for asset_kind. The
// product has no preview / extract support for them today, so the previous
// fine-grained kinds were noise. The set of opaque extensions the upload
// route ACCEPTS now lives in the format registry (see ASSET_UPLOAD_EXTENSIONS
// below); detectAssetKind only needs the families it classifies distinctly.
/**
 * Extension set the asset upload route accepts (bare, no leading dot).
 *
 * The format registry (`src/core/format-registry.ts`) is the single source
 * of truth for this set — it owns the canonical disposition of every format.
 * This re-export keeps `isAssetUploadExtension` and the (untouched) agent
 * `import_file` tool working against the same data, with no parallel list to
 * drift. The registry's `assetUploadExtensions()` reproduces the historical
 * membership exactly (including `.csv`, which the agent tool stores as an
 * opaque asset even though the web path writes it as a dataset).
 */
export const ASSET_UPLOAD_EXTENSIONS = assetUploadExtensions();
export function isAssetUploadExtension(filename) {
    const ext = filename.split('.').pop()?.toLowerCase() || '';
    return ASSET_UPLOAD_EXTENSIONS.has(ext);
}
/**
 * Whether an asset row should be vision-captioned for search: a raster IMAGE
 * that is not SVG (SVG stays transitional until sanitize-html is wired). The
 * single predicate the live caption trigger
 * (`VectorService.ensureInitialAssetIndexState`) and the boot/recovery sweep
 * (`asset-index-sweep.ts`) share, so they can't drift on which assets get
 * captioned.
 */
export function isCaptionableImage(mediaType, filename) {
    if (mediaType !== 'image')
        return false;
    const ext = filename.split('.').pop()?.toLowerCase() || '';
    return ext !== 'svg';
}
// Extension fallbacks for getMediaType — families whose files commonly arrive
// as application/octet-stream (design tools, iWork, legacy font uploads), so
// MIME alone can't classify them. Membership follows the upload registry's
// accepted set (assetUploadExtensions); json/xml/txt/md cover legacy and
// agent-imported rows even though the web upload route doesn't accept them.
const DESIGN_MEDIA_EXTS = new Set(['ai', 'fig', 'idml', 'indd', 'psd', 'sketch', 'xd']);
const ARCHIVE_MEDIA_EXTS = new Set(['zip', 'tar', 'gz', 'tgz', '7z', 'rar']);
const IWORK_MEDIA_EXTS = new Set(['key', 'pages', 'numbers']);
const FONT_MEDIA_EXTS = new Set(['woff', 'woff2', 'ttf', 'otf']);
const DATA_MEDIA_EXTS = new Set(['json', 'xml', 'csv', 'tsv', 'yaml', 'yml']);
const TEXT_MEDIA_EXTS = new Set(['txt', 'md', 'rtf']);
/**
 * Determine media type (the user-facing Library category) from content type,
 * with an extension fallback for octet-stream families. The category is
 * DERIVED at read time wherever it reaches the UI (asset-listing re-computes
 * it from contentType + filename), so refining these rules re-categorizes
 * existing assets with no migration; the stored assets.db `media_type`
 * column remains for internal queries (e.g. the caption sweep's image check).
 */
export function getMediaType(contentType, filename) {
    if (contentType.startsWith('image/'))
        return 'image';
    if (contentType.startsWith('video/'))
        return 'video';
    if (contentType.startsWith('audio/'))
        return 'audio';
    if (contentType.startsWith('font/') || contentType.includes('font'))
        return 'font';
    if (contentType.includes('pdf') ||
        contentType.includes('document') ||
        contentType.includes('word') ||
        contentType.includes('excel') ||
        contentType.includes('powerpoint') ||
        contentType.includes('presentation')) {
        return 'document';
    }
    if (contentType.includes('zip') ||
        contentType.includes('tar') ||
        contentType.includes('gzip') ||
        contentType.includes('7z') ||
        contentType.includes('rar') ||
        contentType.includes('compressed')) {
        return 'archive';
    }
    // Structured/data text before the text/* catch-all so text/csv lands in
    // data, not text.
    if (contentType.includes('json') ||
        contentType.includes('xml') ||
        contentType.includes('csv') ||
        contentType.includes('tab-separated') ||
        contentType.includes('yaml')) {
        return 'data';
    }
    if (contentType.startsWith('text/'))
        return 'text';
    const ext = filename?.split('.').pop()?.toLowerCase() ?? '';
    if (DESIGN_MEDIA_EXTS.has(ext))
        return 'design';
    if (ARCHIVE_MEDIA_EXTS.has(ext))
        return 'archive';
    if (IWORK_MEDIA_EXTS.has(ext))
        return 'document';
    if (FONT_MEDIA_EXTS.has(ext))
        return 'font';
    if (DATA_MEDIA_EXTS.has(ext))
        return 'data';
    if (TEXT_MEDIA_EXTS.has(ext))
        return 'text';
    return 'other';
}
/**
 * Sanitize a filename for use as a storage key segment.
 *
 * Replaces every character that's not `[a-zA-Z0-9._-]` with `_`. Preserves
 * the extension via the `.` allowlist; preserves common separators (`_` and
 * `-`) so display titles derived from the slug stay readable.
 *
 * NOT a security boundary — `validateFilename()` above is the gate that
 * throws on traversal / hidden / forbidden chars / length. `safeFilename`
 * is the post-validation key builder. Promoted from
 * `asset-sidecar-service.ts` so the MCP presign flow can share it.
 */
export function safeFilename(name) {
    return name.replace(/[^a-zA-Z0-9._-]/g, '_');
}
/**
 * Validate filename for security
 */
export function validateFilename(filename) {
    if (!filename || filename.trim() === '') {
        throw new Error('Filename cannot be empty');
    }
    // Check for path traversal
    if (filename.includes('..') ||
        filename.includes('/') ||
        filename.includes('\\')) {
        throw new Error('Filename cannot contain path separators or traversal sequences');
    }
    // Check for hidden files
    if (filename.startsWith('.')) {
        throw new Error('Filename cannot start with a dot (hidden files not allowed)');
    }
    // Check for forbidden characters
    const forbiddenChars = /[<>:"|?*\x00-\x1f]/;
    if (forbiddenChars.test(filename)) {
        throw new Error('Filename contains forbidden characters');
    }
    // Check length
    if (filename.length > 255) {
        throw new Error('Filename too long (max 255 characters)');
    }
}
/**
 * Build asset key from components
 */
export function buildAssetKey(entityType, entityId, filename, keyPrefix) {
    return `${keyPrefix || entityType}/${entityId}/${filename}`;
}
/**
 * Build asset prefix for listing
 */
export function buildAssetPrefix(entityType, entityId, keyPrefix) {
    return `${keyPrefix || entityType}/${entityId}/`;
}
//# sourceMappingURL=base.js.map