"""HTTP routes for self-service artifact downloads — ``/api/v1/cli`` and ``/api/v1/plugin``. Four endpoints, all gated on a logged-in Helix session (any active employee): * ``GET /cli/manifest`` — CLI artifacts available (no S3 keys) * ``GET /cli/download?platform`` — short-lived presigned S3 URL in the JSON envelope (NOT a 302: bearer-token auth would not survive a browser redirect to S3 — see the handler docstring). * ``GET /plugin/manifest`` — Claude Code plugin marketplace zip metadata * ``GET /plugin/download`` — presigned URL for the zip (single artifact, no platform param — the zip is platform-independent) Downloads are pulls, audited per access — there is no notification fan-out and nothing is pushed to employees. """ from __future__ import annotations import uuid from fastapi import APIRouter, Depends, Query, Request from sqlalchemy.ext.asyncio import AsyncSession from app.audit.service import AuditService from app.auth.models import AuthUser from app.core.database import get_db from app.core.dependencies import get_current_user from app.core.exceptions import NotFoundError from app.core.response import success_response from app.domains.cli_dist import service router = APIRouter(prefix="/api/v1/cli", tags=["cli"]) def _trace_id(request: Request) -> uuid.UUID: raw = getattr(request.state, "trace_id", None) try: return uuid.UUID(str(raw)) except (ValueError, TypeError): return uuid.uuid4() @router.get("/manifest", summary="List downloadable helix CLI artifacts") async def get_cli_manifest( request: Request, current_user: AuthUser = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """Return the published CLI version + per-platform download metadata. ``available=false`` (with an empty ``platforms``) before the first release or when storage is unconfigured — the UI hides the download card in that case rather than showing a broken button. """ data = await service.get_manifest(db) return success_response(data=data, request=request) @router.get("/download", summary="Get a download URL for a helix CLI artifact") async def download_cli( request: Request, platform: str = Query(..., description="Platform key, e.g. macos-arm64 / wheel"), current_user: AuthUser = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """Return a short-lived presigned S3 URL (in the envelope) for the requested ``platform``. Deliberately JSON, not a 302: the web UI authenticates with a bearer token attached by the axios interceptor, and a browser navigation to a redirect would NOT carry that header (the access token lives in memory, not a cookie). The frontend instead does ``window.location = data.url`` — the presigned URL is self-authenticating (SigV4). Scripts/MCP can ``jq -r .data.url``. Bare authentication IS sufficient (unlike user-upload reads): every artifact is a public release binary identical for all employees — there is no per-user object to ACL. Each download is still audited. """ result = await service.presign_download(db, platform=platform) if result is None: raise NotFoundError( "Requested CLI artifact is not available for download" ) await AuditService(db).log( trace_id=_trace_id(request), actor_type="user", actor_user_id=current_user.id, action="cli.download", status="success", resource_type="cli_artifact", resource_id=platform, detail={ "version": result.get("version"), "filename": result.get("filename"), "sha256": result.get("sha256"), }, ) return success_response(data=result, request=request) # ── Claude Code plugin (marketplace zip) ───────────────────────────── plugin_router = APIRouter(prefix="/api/v1/plugin", tags=["plugin"]) @plugin_router.get("/manifest", summary="Claude Code plugin download metadata") async def get_plugin_manifest( request: Request, current_user: AuthUser = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """Published plugin version + zip metadata. ``available=false`` when no release exists yet — the Help Center hides the card instead of rendering a broken download button.""" data = await service.get_plugin_manifest(db) return success_response(data=data, request=request) @plugin_router.get("/download", summary="Get a download URL for the plugin zip") async def download_plugin( request: Request, current_user: AuthUser = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """Short-lived presigned S3 URL for the marketplace zip, in the JSON envelope (same non-302 rationale as the CLI download above). The zip is identical for every employee; each download is audited.""" result = await service.presign_plugin_download(db) if result is None: raise NotFoundError( "The Claude Code plugin is not available for download yet" ) await AuditService(db).log( trace_id=_trace_id(request), actor_type="user", actor_user_id=current_user.id, action="plugin.download", status="success", resource_type="plugin_artifact", resource_id="helix-claude-plugin", detail={ "version": result.get("version"), "plugin_version": result.get("plugin_version"), "filename": result.get("filename"), "sha256": result.get("sha256"), }, ) return success_response(data=result, request=request)