/**
 * MCP pre-signed asset upload — STEP 1 of 3.
 *
 * Validates the request, resolves the target folder, signs a PUT URL the
 * agent uploads to directly, and mints an HMAC token the agent later passes
 * back to `finalizeAssetUpload`. Bytes never traverse the model context.
 *
 * Authz: `canWriteRepo` on the resolved repo. For the default-home case
 * (`input.repo` omitted) this lands on the workspace_wide Assets store,
 * which grants write to any authed user. Finalize re-checks `canWriteRepo`
 * because a 15-min token outlives a folder-access revoke.
 *
 * Mime + filename are pinned at presign:
 *   - mime: only registry presign-safe raw assets pass (raster images and
 *     font files — `presignMimeAllowlist()`). SVG is excluded because it carries
 *     `sanitize: 'svg'` and the app never holds the bytes on this path, so it
 *     can't sanitize (V2 — SVG goes through the inline-bytes tool). The chosen
 *     mime is pinned into the PUT signature so the agent must send a matching
 *     Content-Type header or R2 rejects with SignatureDoesNotMatch — the
 *     wire-level guarantee behind "no SVG polyglots."
 *   - filename: validateFilename() (path-traversal / hidden / forbidden /
 *     length cap) → safeFilename() (key-segment safety). Both shared with
 *     the in-process upload path via services/assets/base.ts.
 *
 * See ~/.claude/plans/mcp-presigned-asset-upload.md.
 */
import { randomUUID } from 'node:crypto';
import { Workspace } from '../core/workspace.js';
import { canWriteRepo } from './access-control.js';
import { resolveWorkspaceAssetsRepo } from './workspace-assets-folder.js';
import { validateFilename, safeFilename } from './assets/base.js';
import { presignMimeAllowlist, resolveFormat } from '../core/format-registry.js';
import { signAssetUploadToken } from './asset-upload-token.js';
import { ForbiddenError, InvalidFilenameError, InvalidMimeTypeError, LocalBackendNotSupportedError, } from './asset-upload-errors.js';
import { log } from '../utils/log.js';
/** Registry-derived presign MIME allowlist (P4-V1). Computed once at module
 *  load from `presignMimeAllowlist()` — the format registry is the single
 *  source of truth, so this can't drift from the multipart/inline gates. SVG is
 *  excluded by construction (it carries `sanitize: 'svg'`, so `isPresignSafe`
 *  rejects it — V2). */
const ALLOWED_MIME = new Set(presignMimeAllowlist());
const URL_TTL_SECONDS = 15 * 60;
export async function presignAssetUpload(input) {
    // 1. Mime gate (registry-derived — see ALLOWED_MIME)
    if (!ALLOWED_MIME.has(input.mimeType)) {
        throw new InvalidMimeTypeError(`Unsupported mime type "${input.mimeType}". The presign flow accepts: ${[...ALLOWED_MIME].sort().join(', ')}. ` +
            `Other formats (incl. SVG and documents) must use the inline-bytes upload tool, which sanitizes server-side.`);
    }
    // 2. Filename gate (shared validator → typed error). Runs before the
    //    mime/filename coupling so a malformed/empty filename fails as
    //    InvalidFilenameError, not a confusing mime mismatch.
    try {
        validateFilename(input.filename);
    }
    catch (err) {
        throw new InvalidFilenameError(err instanceof Error ? err.message : 'Invalid filename');
    }
    // 2b. Mime/filename coupling. The mime is pinned into the PUT signature AND
    //     the asset entity derives asset_kind/asset_mime from the FILENAME at
    //     finalize. If the two disagree (e.g. mime=image/heic, filename=a.png),
    //     the stored entity would claim image/png while the bytes are heic, and
    //     the caption path would ship the wrong mime. Require the declared mime
    //     to be one the filename's registry format actually carries.
    if (!resolveFormat(input.filename).mimeTypes.includes(input.mimeType)) {
        throw new InvalidMimeTypeError(`Mime type "${input.mimeType}" does not match the extension of "${input.filename}". ` +
            `Pass a filename whose extension matches the content type.`);
    }
    // 3. Resolve target repo. resolveWorkspaceAssetsRepo (not the lower-level
    //    ensureWorkspaceAssetsFolder) so workspaceManager.reloadConfig() runs
    //    after first-write provisioning — otherwise wm.getGraph(repo) would
    //    miss the freshly-created folder later.
    const workspace = new Workspace(input.workspaceManager.getWorkspacePath());
    const repo = input.repo
        ?? (await resolveWorkspaceAssetsRepo(workspace, input.workspaceManager));
    // 4. Authz on the resolved repo
    if (!canWriteRepo(input.user, repo, input.workspaceManager, input.authService)) {
        throw new ForbiddenError(`No write access to folder "${repo}"`);
    }
    // 5. Backend must support the full presign lifecycle (PUT + HEAD + copy)
    let graph;
    try {
        graph = input.workspaceManager.getGraph(repo);
    }
    catch {
        throw new ForbiddenError(`Folder "${repo}" not found`);
    }
    const assetService = graph.getAssetService();
    if (!assetService || !assetService.supportsPresignedUploads()) {
        throw new LocalBackendNotSupportedError('Pre-signed uploads require an R2-backed asset store. This workspace is using local filesystem storage; upload via the multipart endpoint (POST /api/repos/:repo/assets) instead.');
    }
    // 6. Mint identifiers
    const assetId = randomUUID();
    const filename = safeFilename(input.filename);
    // 7. Sign PUT URL — ContentType is pinned in the signature
    const { uploadUrl, headers } = await assetService.signPendingPutUrl(assetId, filename, input.mimeType, URL_TTL_SECONDS);
    // 8. Owner shape
    const ownerType = input.entity?.type ?? 'asset';
    const ownerId = input.entity?.id ?? assetId;
    // 9. Sign token (15-min TTL matches the URL — see asset-upload-token.ts)
    const token = signAssetUploadToken({
        assetId,
        repo,
        ownerType,
        ownerId,
        uploaderUserId: String(input.user.id),
        mime: input.mimeType,
        filename,
    }, input.authService.getJwtSecret());
    // 10. Audit log (R3) — single line, key=value-ish so a grep can reconstruct
    //     the flow.
    log.info('[asset-presign]', {
        event: 'asset-presign',
        userId: String(input.user.id),
        repo,
        ownerType,
        ownerId,
        assetId,
        mime: input.mimeType,
        filename,
    });
    const expiresAt = new Date(Date.now() + URL_TTL_SECONDS * 1000).toISOString();
    return { assetId, uploadUrl, headers, token, expiresAt, repo };
}
//# sourceMappingURL=asset-presign-service.js.map