def create_signed_upload_url( jwt_token: str, *, bucket: str, object_path: str, expires_in: Optional[int] = None, ) -> Dict[str, Any]: """Return a signed PUT URL so external workers can upload directly to Supabase.""" resolved_user_id = resolve_user_id(jwt_token) normalized_path = str(object_path or "").lstrip("/") if not normalized_path: raise ValueError("object_path is required for signed uploads") # SECURITY FEATURE: ensure callers only sign uploads inside their own namespace. if not normalized_path.startswith(f"{resolved_user_id}/"): logger.warning( "Attempted to create signed upload URL outside user namespace", extra={"resolved_user_id": resolved_user_id, "object_path": object_path}, ) raise HTTPException(status_code=403, detail="Access denied for requested upload path") client = get_client(jwt_token) # Ensure Storage requests are authenticated as the user so RLS applies at sign time if jwt_token and hasattr(client, "storage") and hasattr(client.storage, "session"): try: client.storage.session.headers["Authorization"] = f"Bearer {jwt_token}" except Exception: logger.debug("Unable to set storage authorization header during sign", exc_info=True) response = client.storage.from_(bucket).create_signed_upload_url(normalized_path) signed_url = response.get("signedURL") or response.get("signed_url") token = response.get("token") # Supabase currently fixes signed upload URLs to a 2-hour lifetime; the API # does not accept custom expirations, so expires_in is informational only. ttl = expires_in or 7200 if not signed_url or not token: logger.error( "Signed upload URL generation failed", extra={ "bucket": bucket, "storage_path": normalized_path, }, ) raise ValueError("Signed upload URL generation failed") result = { "bucket": bucket, "path": normalized_path, "signed_url": signed_url, "token": token, "expiry_seconds": ttl, } # Log without leaking the token; record bucket/path/ttl only try: logger.info( "storage signed PUT URL minted bucket=%s path=%s expiry_seconds=%d", bucket, normalized_path, ttl, ) except Exception: pass return result